Auto-sync: 2026-06-25 22:43:25 (part 8)
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .gitattributes +4 -0
- outputs/audit_venv/lib/python3.11/site-packages/networkx-3.6.1+computecanada.dist-info/INSTALLER +1 -0
- outputs/audit_venv/lib/python3.11/site-packages/networkx-3.6.1+computecanada.dist-info/METADATA +177 -0
- outputs/audit_venv/lib/python3.11/site-packages/networkx-3.6.1+computecanada.dist-info/RECORD +0 -0
- outputs/audit_venv/lib/python3.11/site-packages/networkx-3.6.1+computecanada.dist-info/WHEEL +5 -0
- outputs/audit_venv/lib/python3.11/site-packages/networkx-3.6.1+computecanada.dist-info/entry_points.txt +2 -0
- outputs/audit_venv/lib/python3.11/site-packages/networkx-3.6.1+computecanada.dist-info/licenses/LICENSE.txt +37 -0
- outputs/audit_venv/lib/python3.11/site-packages/networkx-3.6.1+computecanada.dist-info/top_level.txt +1 -0
- outputs/audit_venv/lib/python3.11/site-packages/networkx/algorithms/flow/tests/wlm3.gpickle.bz2 +3 -0
- outputs/audit_venv/lib/python3.11/site-packages/networkx/algorithms/flow/utils.py +194 -0
- outputs/audit_venv/lib/python3.11/site-packages/networkx/algorithms/graph_hashing.py +435 -0
- outputs/audit_venv/lib/python3.11/site-packages/networkx/algorithms/graphical.py +483 -0
- outputs/audit_venv/lib/python3.11/site-packages/networkx/algorithms/hierarchy.py +57 -0
- outputs/audit_venv/lib/python3.11/site-packages/networkx/algorithms/hybrid.py +196 -0
- outputs/audit_venv/lib/python3.11/site-packages/networkx/algorithms/isolate.py +107 -0
- outputs/audit_venv/lib/python3.11/site-packages/networkx/algorithms/isomorphism/__init__.py +7 -0
- outputs/audit_venv/lib/python3.11/site-packages/networkx/algorithms/isomorphism/ismags.py +1306 -0
- outputs/audit_venv/lib/python3.11/site-packages/networkx/algorithms/isomorphism/isomorph.py +336 -0
- outputs/audit_venv/lib/python3.11/site-packages/networkx/algorithms/isomorphism/isomorphvf2.py +1262 -0
- outputs/audit_venv/lib/python3.11/site-packages/networkx/algorithms/isomorphism/matchhelpers.py +352 -0
- outputs/audit_venv/lib/python3.11/site-packages/networkx/algorithms/isomorphism/temporalisomorphvf2.py +308 -0
- outputs/audit_venv/lib/python3.11/site-packages/networkx/algorithms/isomorphism/tests/__init__.py +0 -0
- outputs/audit_venv/lib/python3.11/site-packages/networkx/algorithms/isomorphism/tests/iso_r01_s80.A99 +0 -0
- outputs/audit_venv/lib/python3.11/site-packages/networkx/algorithms/isomorphism/tests/iso_r01_s80.B99 +0 -0
- outputs/audit_venv/lib/python3.11/site-packages/networkx/algorithms/isomorphism/tests/si2_b06_m200.A99 +0 -0
- outputs/audit_venv/lib/python3.11/site-packages/networkx/algorithms/isomorphism/tests/si2_b06_m200.B99 +0 -0
- outputs/audit_venv/lib/python3.11/site-packages/networkx/algorithms/isomorphism/tests/test_ismags.py +719 -0
- outputs/audit_venv/lib/python3.11/site-packages/networkx/algorithms/isomorphism/tests/test_isomorphism.py +103 -0
- outputs/audit_venv/lib/python3.11/site-packages/networkx/algorithms/isomorphism/tests/test_isomorphvf2.py +490 -0
- outputs/audit_venv/lib/python3.11/site-packages/networkx/algorithms/isomorphism/tests/test_match_helpers.py +64 -0
- outputs/audit_venv/lib/python3.11/site-packages/networkx/algorithms/isomorphism/tests/test_temporalisomorphvf2.py +212 -0
- outputs/audit_venv/lib/python3.11/site-packages/networkx/algorithms/isomorphism/tests/test_tree_isomorphism.py +202 -0
- outputs/audit_venv/lib/python3.11/site-packages/networkx/algorithms/isomorphism/tests/test_vf2pp.py +1655 -0
- outputs/audit_venv/lib/python3.11/site-packages/networkx/algorithms/isomorphism/tests/test_vf2pp_helpers.py +3118 -0
- outputs/audit_venv/lib/python3.11/site-packages/networkx/algorithms/isomorphism/tests/test_vf2userfunc.py +196 -0
- outputs/audit_venv/lib/python3.11/site-packages/networkx/algorithms/isomorphism/tree_isomorphism.py +264 -0
- outputs/audit_venv/lib/python3.11/site-packages/networkx/algorithms/isomorphism/vf2pp.py +1102 -0
- outputs/audit_venv/lib/python3.11/site-packages/networkx/algorithms/isomorphism/vf2userfunc.py +192 -0
- outputs/audit_venv/lib/python3.11/site-packages/networkx/algorithms/link_analysis/__init__.py +2 -0
- outputs/audit_venv/lib/python3.11/site-packages/networkx/algorithms/link_analysis/hits_alg.py +337 -0
- outputs/audit_venv/lib/python3.11/site-packages/networkx/algorithms/link_analysis/pagerank_alg.py +498 -0
- outputs/audit_venv/lib/python3.11/site-packages/networkx/algorithms/link_analysis/tests/__init__.py +0 -0
- outputs/audit_venv/lib/python3.11/site-packages/networkx/algorithms/link_analysis/tests/test_hits.py +77 -0
- outputs/audit_venv/lib/python3.11/site-packages/networkx/algorithms/link_analysis/tests/test_pagerank.py +213 -0
- outputs/audit_venv/lib/python3.11/site-packages/networkx/algorithms/link_prediction.py +687 -0
- outputs/audit_venv/lib/python3.11/site-packages/networkx/algorithms/lowest_common_ancestors.py +280 -0
- outputs/audit_venv/lib/python3.11/site-packages/networkx/algorithms/matching.py +1148 -0
- outputs/audit_venv/lib/python3.11/site-packages/networkx/algorithms/minors/__init__.py +27 -0
- outputs/audit_venv/lib/python3.11/site-packages/networkx/algorithms/minors/contraction.py +738 -0
- 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))
|