├── .github └── workflows │ └── test.yml ├── .gitignore ├── COPYING ├── Changelog ├── Makefile.old ├── README.rst ├── misc ├── epydoc.css ├── logo.png └── logo.txt ├── poetry.lock ├── pygraph ├── __init__.py ├── algorithms │ ├── __init__.py │ ├── accessibility.py │ ├── critical.py │ ├── cycles.py │ ├── filters │ │ ├── __init__.py │ │ ├── find.py │ │ ├── null.py │ │ └── radius.py │ ├── generators.py │ ├── heuristics │ │ ├── __init__.py │ │ ├── chow.py │ │ └── euclidean.py │ ├── minmax.py │ ├── pagerank.py │ ├── searching.py │ ├── sorting.py │ ├── traversal.py │ └── utils.py ├── classes │ ├── __init__.py │ ├── digraph.py │ ├── exceptions.py │ ├── graph.py │ ├── hypergraph.py │ └── unionfind.py ├── mixins │ ├── __init__.py │ ├── basegraph.py │ ├── common.py │ └── labeling.py └── readwrite │ ├── __init__.py │ ├── dot.py │ └── markup.py ├── pyproject.toml └── tests ├── __init__.py ├── test_data.py ├── testlib.py ├── unittests-accessibility.py ├── unittests-critical.py ├── unittests-cycles.py ├── unittests-digraph.py ├── unittests-filters.py ├── unittests-graph.py ├── unittests-heuristics.py ├── unittests-hypergraph.py ├── unittests-minmax.py ├── unittests-pagerank.py ├── unittests-readwrite.py ├── unittests-searching.py └── unittests-sorting.py /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ${{ matrix.os }} 8 | strategy: 9 | # fail-fast: true 10 | max-parallel: 5 11 | matrix: 12 | python-version: ['3.10', '3.11', '3.12', '3.13'] 13 | os: [ ubuntu-latest, macos-latest ] 14 | steps: 15 | - uses: actions/checkout@v3 16 | - name: Set up Python ${{ matrix.python-version }} 17 | uses: actions/setup-python@v3 18 | with: 19 | python-version: ${{ matrix.python-version }} 20 | - name: Install dependencies 21 | run: | 22 | pip install --upgrade pip poetry 23 | poetry install --with dev 24 | - name: PyTest testrunner 25 | run: | 26 | poetry run pytest 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.swp 3 | .#* 4 | .cache 5 | __pycache__ 6 | .eggs 7 | *.po~ 8 | *.mo 9 | .idea 10 | .vscode 11 | *.wpr 12 | *.wpu 13 | .venv 14 | .coverage 15 | venv/ 16 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | Copyright (c) 2007-2012 Pedro Matiello 2 | Christian Muise 3 | Eugen Zagorodniy 4 | Johannes Reinhardt 5 | Nathan Davis 6 | Paul Harrison 7 | Rhys Ulerich 8 | Roy Smith 9 | Salim Fadhley 10 | Tomaz Kovacic 11 | Zsolt Haraszti 12 | 13 | Permission is hereby granted, free of charge, to any person 14 | obtaining a copy of this software and associated documentation 15 | files (the "Software"), to deal in the Software without 16 | restriction, including without limitation the rights to use, 17 | copy, modify, merge, publish, distribute, sublicense, and/or sell 18 | copies of the Software, and to permit persons to whom the 19 | Software is furnished to do so, subject to the following 20 | conditions: 21 | 22 | The above copyright notice and this permission notice shall be 23 | included in all copies or substantial portions of the Software. 24 | 25 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 26 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 27 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 28 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 29 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 30 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 31 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 32 | OTHER DEALINGS IN THE SOFTWARE. 33 | -------------------------------------------------------------------------------- /Changelog: -------------------------------------------------------------------------------- 1 | python-graph 2 | A library for working with graphs in Python 3 | -------------------------------------------------------------------------------- 4 | 5 | CHANGELOG 6 | 7 | Unreleased 2.0.0: 8 | 9 | Backwards incompatibilities: 10 | Needs at least python3.10 11 | Package unified, install pydot if you need it or install python-graph[dot] 12 | 13 | Enhancements: 14 | New location, and new main maintainers. 15 | Various fixes and improvements to the Makefile and test running. 16 | Automatic tests and coverage of master and pull request with Travis and Coveralls. 17 | 2.0.1 (unreleased) 18 | 19 | 20 | - Nothing changed yet. 21 | 22 | 23 | 2.0.0 (2025-06-13) 24 | Make graph.add_edge run in O(1) instead of O(degree(V)) (dkorduban) 25 | Added implementation of Kruskal's Minimum Spanning Tree construction algorithm (goldragoon) 26 | Repackaged and updated to python3 (robinharms) 27 | 28 | Release 1.8.2 [July 14, 2012] 29 | 30 | Fixes: 31 | The find_cycle function now accepts instances of any subtype of graph and digraph. 32 | 33 | 34 | Release 1.8.1 [Jan 08, 2012] 35 | 36 | Enhancements: 37 | Shortest-path now executes in O(n*log(n)) instead of O(n^2). 38 | 39 | Fixes: 40 | Shortest-path raises KeyError when the source node is not on the graph; 41 | Bellman-Ford algorithm now works for unconnected graphs (Issue 87); 42 | Linking, unlinking and relinking a hypernode to hyperedge now works as expected (Issue 86); 43 | Graph comparison does not raise exceptions anymore (Issue 88); 44 | Fixed undesired sharing of attribute lists (Issue 92); 45 | Fixed reading of XML-stored graphs with edge attributes; 46 | Fixed calculation of minimal spanning trees of graphs with negative-weighted edges (Issue 102). 47 | 48 | 49 | Release 1.8.0 [Oct 01, 2010] 50 | 51 | Enhancements: 52 | Added Pagerank algorithm; 53 | Added Gomory-Hu cut-tree algorithm. 54 | 55 | Fixes: 56 | Edges from one node to itself are no longer duplicated (Issue 75). 57 | 58 | 59 | Release 1.7.0 [Mar 20, 2010] 60 | 61 | Enhancements: 62 | Added equality test for graphs, digraphs and hypergraphs; 63 | Added has_edge() and has_hyperedge() methods to hypergraph objects; 64 | Accepting subtypes of graph, digraph and hypergraph in dot-language output (Issue 64); 65 | Added Bellman-Ford algorithm; 66 | Added Edmonds-Karp Maximum-Flow algorithm. 67 | 68 | Fixes: 69 | Adding an edge with a label to a digraph now works again; 70 | Deleting an edge of a hypergraph now deletes its attributes; 71 | Node attributes on hypergraphs work now; 72 | Checking for node equality correctly in find_cycle; 73 | Avoiding errors caused by deep recursion on many algorithms (Issue 66). 74 | 75 | 76 | Release 1.6.3 [Dec 13, 2009] 77 | 78 | Enhancements: 79 | Added Python3 support (support for Python 2.5 and lower was dropped). 80 | 81 | Fixes: 82 | Adding a graph to a digraph now works (Issue 39); 83 | Fixed the reading of graphs and digraphs stored in XML. 84 | 85 | Important API Changes: 86 | Edges are now passed around as generic objects. In the case of graph / digraph this is a tuple; 87 | Removed traversal() method from graph and digraph classes; 88 | Removed accessibility, connected_components, cut_nodes and cut_hyperedges from hypergraph class; 89 | Functions for reading a hypergraph doesn't take an empty hypergraph as argument anymore. 90 | 91 | 92 | Release 1.6.2 [Sep 30, 2009] 93 | 94 | Important API Changes: 95 | Adding an arrow to an non existing node on a digraph now fails sanely (Issue 35); 96 | Adding an already added node to a graph or digraph now raises an exception (AdditionError); 97 | Adding an already added edge to a graph or digraph now raises an exception (AdditionError); 98 | pygraph.classes.Classname.classname classes were renamed to pygraph.classes.classname.classname; 99 | pygraph.algorithms.filters.Filtername.filtername filters were renamed to pygraph.algorithms.filters.filtername.filtername; 100 | pygraph.algorithms.heuristics.Heuristicname.heuristicname heuristics were renamed to pygraph.algorithms.heuristics.heuristicname.heuristicname; 101 | hypergraph's read() and write() methods were removed. 102 | 103 | 104 | Release 1.6.1 [Jul 04, 2009] 105 | 106 | Enhancements: 107 | Added reverse method to the digraph class. 108 | 109 | Important API Changes: 110 | Removed methods calling algorithms from graph and digraph classes; 111 | pygraph.algorithms.cycles.find_cycle does not take argument directed anymore; 112 | Removed methods read, write and generate from graph and digraph classes; 113 | Functions for writing and reading graphs now in pygraph.readwrite. 114 | 115 | 116 | Release 1.6.0 [Jun 06, 2009] 117 | 118 | Important API Changes: 119 | Module name was renamed to pygraph; 120 | python_graph_exception was renamed to GraphError; 121 | Exception unreachable was renamed to NodeUnreachable; 122 | get_edge_weight was renamed to edge_weight; 123 | get_edge_label was renamed to edge_label; 124 | get_edge_attributes was renamed to edge_attributes; 125 | get_node_attributes was renamed to node_attributes; 126 | degree was renamed to node_degree; 127 | order was renamed to node_order. 128 | 129 | 130 | Release 1.5.0 [May 03, 2009] 131 | 132 | Enhancements: 133 | Assymptotically faster Mutual Accessibility (now using Tarjan's algorithm); 134 | DOT-Language importing; 135 | Transitive edge detection; 136 | Critical path algorithm. 137 | 138 | Fixes: 139 | Cycle detection algorithm was reporting wrong results on some digraphs; 140 | Removed Minimal Spanning Tree from Digraphs as Prim's algorithm does not work on them (Issue 28). 141 | Deletion of A--A edge raised an exception; 142 | Deletion of an node with an A--A edge raised an exception. 143 | 144 | Important API Changes: 145 | Removed minimal_spanning_tree() method from the digraph class. 146 | 147 | 148 | Release 1.4.2 [Feb 22, 2009] 149 | 150 | Fixes: 151 | find_cycle() trapped itself in infinite recursion in some digraphs (Issue 22). 152 | 153 | 154 | Release 1.4.1 [Feb 09, 2009] 155 | 156 | Fixes: 157 | graph.algorithms.filters was not being installed (Issue 20). 158 | 159 | 160 | Release 1.4.0 [Feb 07, 2009] 161 | 162 | Enhancements: 163 | Added Asearch algorithm (as heuristic_search); 164 | Added Chow's and Euclidean heuristics for A*; 165 | Added filtered depth-first and breadth-first search; 166 | Added 'find' search filter (stops the search after reaching a target node); 167 | Added 'radius' search filter (radial limit for searching); 168 | Moved to setuptools. 169 | 170 | Fixes: 171 | Breadth first search was omitting the first node in level ordering when no root was specified. 172 | 173 | 174 | Release 1.3.1 [Oct 27, 2008] 175 | 176 | Fixes: 177 | Graph and digraph inverse was not working; 178 | Node removal in digraphs was not deleting all relevant edges (Issue 13). 179 | 180 | Important API Changes: 181 | Deprecated methods were removed. 182 | 183 | 184 | Release 1.3.0 [Sep 28, 2008] 185 | 186 | Enhancements: 187 | Tree traversals (preorder and postorder). 188 | 189 | Fixes: 190 | Node insertion is much faster now (Issue 11). 191 | Hypernode/hyperedge insertion also much faster. 192 | 193 | Important API Changes: 194 | get_nodes() is now nodes(); 195 | get_edges() is now edges(); 196 | get_neighbors() is now neighbors(); 197 | get_incidents() is now incidents(); 198 | get_order() is now order(); 199 | get_degree() is now degree(). 200 | (Former method names are deprecated and will be removed in the next release.) 201 | 202 | 203 | Release 1.2.0 [Sep 09, 2008] 204 | 205 | Enhancements: 206 | Moved to new class style; 207 | Graphs and digraphs are separated classes now; 208 | Added level-based ordering to breadth first search; 209 | Graph object is now iterable; 210 | Graph object is now a container and graphobj[nodeid] iterates too; 211 | Support for node and edge attributes (Issue 5); 212 | Node deletion. 213 | 214 | Fixes: 215 | Install now works with a prefix (Issue 10); 216 | Shortest path spanning trees did not had an explicit root. 217 | 218 | Important API Changes: 219 | breadth_first_search() now returns a tuple; 220 | Arrow methods are gone. Use class digraph + edge methods for directed graphs now. 221 | 222 | 223 | Release 1.1.1 [Sep 04, 2008] 224 | 225 | Enhancements: 226 | Improved install script. 227 | 228 | Fixes: 229 | DOT Language output now works for nodes/edges labelled with spaces. 230 | 231 | Important API Changes: 232 | get_neighbours() is now get_neighbors() (Issue 9). 233 | 234 | 235 | Release 1.1.0 [Aug 31, 2008] 236 | 237 | Enhancements: 238 | Hypergraph support (Issue 4); 239 | Complete and complement graph generation; 240 | Weights in random generated graphs (Issue 8). 241 | 242 | Fixes: 243 | Fixed bug in cut-node identification; 244 | Fixed bug causing wrong results for graphs with nodes labelled with values that evaluate to False (Issue 7). 245 | 246 | Important API Changes: 247 | get_edges() now return all edges in the graph; 248 | get_neighbours() has the former behaviour of get_edges(). 249 | 250 | 251 | Release 1.0.0 [Aug 01, 2008] 252 | 253 | Adds some operations; 254 | Random graph generation; 255 | Cut-vertex/cut-edge identification. 256 | 257 | 258 | Release 0.85 [Jul 27, 2008] 259 | 260 | Adds DOT-Language output (Issue 1); 261 | Install script included (Issue 3). 262 | 263 | 264 | Release 0.75 [Jul 06, 2008] 265 | 266 | Added XML import/export; 267 | Docs are bundled now. 268 | 269 | 270 | Release 0.65 [Jun 25, 2008] 271 | 272 | DFS, BFS and MST can be generated for given roots; 273 | Added Dijkstra's shortest path algorithm (Issue 2). 274 | 275 | 276 | Release 0.50 [May 13, 2008] 277 | 278 | Some API changes; 279 | Nodes can now be arbitrary names/objects. 280 | 281 | 282 | Release 0.45 [May 12, 2008] 283 | 284 | Adds Prim's minimal spanning tree. 285 | 286 | 287 | Release 0.40 [Mar 09, 2008] 288 | 289 | Adds topological sorting; 290 | Support for weighted graphs. 291 | 292 | 293 | Release 0.30 [Aug 30, 2007] 294 | 295 | Adds algorithms for accessibility and mutual accessibility. 296 | 297 | 298 | Release 0.20 [Jul 16, 2007] 299 | 300 | Adds breadth-first search; 301 | API documentation. 302 | 303 | 304 | Release 0.10 [Jul 10, 2007] 305 | 306 | First release; 307 | Feat. basic operations and depth-first searching. 308 | -------------------------------------------------------------------------------- /Makefile.old: -------------------------------------------------------------------------------- 1 | # python-graph 2 | # Makefile 3 | # Note! This file is only kept as a reference 4 | 5 | 6 | # Module directories ------------------------------------------------- 7 | 8 | CORE_DIR="core/" 9 | DOT_DIR="dot/" 10 | TESTS_DIR="tests/" 11 | DOCS_DIR="docs/" 12 | TEMP="temp/" 13 | PYTHONPATH="`pwd`/core:`pwd`/dot" 14 | PYTHON="python" 15 | PYTHON3="python3" 16 | 17 | 18 | # General ------------------------------------------------------------ 19 | 20 | nothing: 21 | 22 | sdist: clean sdist-core sdist-dot 23 | rm -rf dist 24 | mkdir dist 25 | cp */dist/* dist 26 | 27 | 28 | # Core --------------------------------------------------------------- 29 | 30 | install-core: 31 | cd ${CORE_DIR} && ./setup.py install 32 | 33 | sdist-core: clean 34 | cd ${CORE_DIR} && ./setup.py sdist 35 | 36 | 37 | # Dot ---------------------------------------------------------------- 38 | 39 | install-dot: 40 | cd ${DOT_DIR} && ./setup.py install 41 | 42 | sdist-dot: clean 43 | cd ${DOT_DIR} && ./setup.py sdist 44 | 45 | 46 | # Docs --------------------------------------------------------------- 47 | 48 | docs: cleanpyc 49 | rm -rf ${DOCS_DIR} ${TEMP} 50 | mkdir -p ${TEMP} 51 | cp -R ${CORE_DIR}/pygraph ${TEMP} 52 | cp -Rn ${DOT_DIR}/pygraph ${TEMP}/pygraph/ 53 | epydoc -v --no-frames --no-sourcecode --name="python-graph" \ 54 | --url="https://github.com/Shoobx/python-graph" \ 55 | --inheritance listed --no-private --html \ 56 | --graph classtree \ 57 | --css misc/epydoc.css -o docs ${TEMP}/pygraph/*.py \ 58 | ${TEMP}/pygraph/algorithms/*py \ 59 | ${TEMP}/pygraph/algorithms/heuristics/*.py \ 60 | ${TEMP}/pygraph/algorithms/filters/* \ 61 | ${TEMP}/pygraph/readwrite/* \ 62 | ${TEMP}/pygraph/classes/*.py \ 63 | ${TEMP}/pygraph/mixins/*.py 64 | rm -rf ${TEMP} 65 | 66 | 67 | # Tests -------------------------------------------------------------- 68 | 69 | test-pre: 70 | reset 71 | 72 | test: test-pre 73 | PYTHONPATH=${PYTHONPATH} ${PYTHON} tests/testrunner.py 74 | 75 | test3: test-pre 76 | PYTHONPATH=${PYTHONPATH} ${PYTHON3} tests/testrunner.py 77 | 78 | tests: test 79 | 80 | 81 | # Tests -------------------------------------------------------------- 82 | 83 | console: 84 | export PYTHONPATH=${PYTHONPATH} && cd ${TESTS_DIR} && ${PYTHON} 85 | 86 | console3: 87 | export PYTHONPATH=${PYTHONPATH} && cd ${TESTS_DIR} && ${PYTHON3} 88 | 89 | 90 | # Cleaning ----------------------------------------------------------- 91 | 92 | cleanpyc: 93 | find tests dot core -name *.pyc -exec rm {} \; 94 | find tests dot core -name __pycache__ -exec rm -rf {} \; -prune 95 | 96 | clean: cleanpyc 97 | rm -rf ${DOCS_DIR} 98 | rm -rf */dist 99 | rm -rf */build 100 | rm -rf */*.egg-info 101 | rm -rf dist 102 | 103 | 104 | # Phony rules -------------------------------------------------------- 105 | 106 | .PHONY: clean cleanpyc docs-core 107 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ============ 2 | python-graph 3 | ============ 4 | 5 | 6 | A library for working with graphs in Python 7 | ------------------------------------------- 8 | 9 | This software provides a suitable data structure for representing graphs and a 10 | whole set of important algorithms. 11 | 12 | 13 | INSTALLING 14 | ---------- 15 | 16 | To install the core module, run: 17 | 18 | .. code-block:: 19 | 20 | pip install python-graph 21 | 22 | To install the dot language support, run: 23 | 24 | .. code-block:: 25 | 26 | pip install python-graph[dot] 27 | 28 | If you want the development version, use poetry. This will also install pytest and pydot. 29 | 30 | .. code-block:: 31 | 32 | pip install poetry 33 | poetry install --with dev 34 | 35 | And to run tests: 36 | 37 | .. code-block:: 38 | 39 | pytest 40 | 41 | Coverage has some defaults set so simply run: 42 | 43 | .. code-block:: 44 | 45 | coverage run 46 | coverage report 47 | 48 | 49 | DOCUMENTATION 50 | ------------- 51 | 52 | FIXME: Module documentation isn't available 53 | 54 | 55 | WEBSITE 56 | ------- 57 | 58 | The latest version of this package can be found at: 59 | 60 | https://github.com/Shoobx/python-graph 61 | 62 | Please report bugs at: 63 | 64 | https://github.com/Shoobx/python-graph/issues 65 | 66 | 67 | PROJECT COMMITTERS 68 | ------------------ 69 | 70 | Pedro Matiello 71 | * Original author; 72 | * Graph, Digraph and Hipergraph classes; 73 | * Accessibility algorithms; 74 | * Cut-node and cut-edge detection; 75 | * Cycle detection; 76 | * Depth-first and Breadth-first searching; 77 | * Minimal Spanning Tree (Prim's algorithm); 78 | * Random graph generation; 79 | * Topological sorting; 80 | * Traversals; 81 | * XML reading/writing; 82 | * Refactoring. 83 | 84 | Christian Muise 85 | * Dot file reading/writing; 86 | * Hypergraph class; 87 | * Refactoring. 88 | 89 | Salim Fadhley 90 | * Porting of Roy Smith's A* implementation to python-graph; 91 | * Edmond Chow's heuristic for A*; 92 | * Refactoring. 93 | 94 | Tomaz Kovacic 95 | * Transitive edge detection; 96 | * Critical path algorithm; 97 | * Bellman-Ford algorithm; 98 | * Logo design. 99 | 100 | 101 | CONTRIBUTORS 102 | ------------ 103 | 104 | Eugen Zagorodniy 105 | * Mutual Accessibility (Tarjan's Algorithm). 106 | 107 | Johannes Reinhardt 108 | * Maximum-flow algorithm; 109 | * Gomory-Hu cut-tree algorithm; 110 | * Refactoring. 111 | 112 | Juarez Bochi 113 | * Pagerank algorithm. 114 | 115 | Nathan Davis 116 | * Faster node insertion. 117 | 118 | Paul Harrison 119 | * Mutual Accessibility (Tarjan's Algorithm). 120 | 121 | Peter Sagerson 122 | * Performance improvements on shortest path algorithm. 123 | 124 | Rhys Ulerich 125 | * Dijkstra's Shortest path algorithm. 126 | 127 | Roy Smith 128 | * Heuristic Searching (A* algorithm). 129 | 130 | Zsolt Haraszti 131 | * Weighted random generated graphs. 132 | 133 | Anand Jeyahar 134 | * Edge deletion on hypergraphs (bug fix). 135 | 136 | Emanuele Zattin 137 | * Hyperedge relinking (bug fix). 138 | 139 | Jonathan Sternberg 140 | * Graph comparison (bug fix); 141 | * Proper isolation of attribute lists (bug fix). 142 | 143 | Daniel Merritt 144 | * Fixed reading of XML-stored graphs with edge attributes. 145 | 146 | Sandro Tosi 147 | * Some improvements to Makefile 148 | 149 | Robin Harms Oredsson 150 | * Py3-fixes and modern distribution. 151 | * Unified package with optional install instead. 152 | 153 | LICENSE 154 | ------- 155 | 156 | This software is provided under the MIT license. See accompanying COPYING file 157 | for details. 158 | -------------------------------------------------------------------------------- /misc/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Shoobx/python-graph/0ec006fa7fa58f1e40dfa94b1bdfd9ae78837da7/misc/logo.png -------------------------------------------------------------------------------- /misc/logo.txt: -------------------------------------------------------------------------------- 1 | LICENCE 2 | 3 | The image is provided under Creative Commons Attribution-Share Alike 3.0 4 | license. Check http://creativecommons.org/licenses/by-sa/3.0/ for more 5 | information. 6 | 7 | ATTRIBUTION: 8 | The artwork for the official logo of python-graph 9 | was designed by Tomaž Kovačič 10 | . 11 | 12 | 13 | -------------------------------------------------------------------------------- /pygraph/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2007-2012 Pedro Matiello 2 | # 3 | # Permission is hereby granted, free of charge, to any person 4 | # obtaining a copy of this software and associated documentation 5 | # files (the "Software"), to deal in the Software without 6 | # restriction, including without limitation the rights to use, 7 | # copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | # copies of the Software, and to permit persons to whom the 9 | # Software is furnished to do so, subject to the following 10 | # conditions: 11 | 12 | # The above copyright notice and this permission notice shall be 13 | # included in all copies or substantial portions of the Software. 14 | 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | # OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | 25 | """ 26 | B{python-graph} 27 | 28 | A library for working with graphs in Python. 29 | 30 | @version: 1.8.2 31 | 32 | L{Data structure} classes are located at C{pygraph.classes}. 33 | 34 | L{Exception} classes are located at C{pygraph.classes.exceptions}. 35 | 36 | L{Search filters} are located at C{pygraph.algorithms.filters}. 37 | 38 | L{Heuristics} for the A* algorithm are exposed in 39 | C{pygraph.algorithms.heuristics}. 40 | 41 | A quick introductory example: 42 | 43 | >>> # Import the module and instantiate a graph object 44 | >>> from pygraph.classes.graph import graph 45 | >>> from pygraph.algorithms.searching import depth_first_search 46 | >>> gr = graph() 47 | >>> # Add nodes 48 | >>> gr.add_nodes(['X','Y','Z']) 49 | >>> gr.add_nodes(['A','B','C']) 50 | >>> # Add edges 51 | >>> gr.add_edge(('X','Y')) 52 | >>> gr.add_edge(('X','Z')) 53 | >>> gr.add_edge(('A','B')) 54 | >>> gr.add_edge(('A','C')) 55 | >>> gr.add_edge(('Y','B')) 56 | >>> # Depth first search rooted on node X 57 | >>> st, pre, post = depth_first_search(gr, root='X') 58 | >>> # Print the spanning tree 59 | >>> {x:st[x] for x in sorted(st)} 60 | {'A': 'B', 'B': 'Y', 'C': 'A', 'X': None, 'Y': 'X', 'Z': 'X'} 61 | """ 62 | -------------------------------------------------------------------------------- /pygraph/algorithms/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2008-2009 Pedro Matiello 2 | # 3 | # Permission is hereby granted, free of charge, to any person 4 | # obtaining a copy of this software and associated documentation 5 | # files (the "Software"), to deal in the Software without 6 | # restriction, including without limitation the rights to use, 7 | # copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | # copies of the Software, and to permit persons to whom the 9 | # Software is furnished to do so, subject to the following 10 | # conditions: 11 | 12 | # The above copyright notice and this permission notice shall be 13 | # included in all copies or substantial portions of the Software. 14 | 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | # OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | 25 | """ 26 | Algorithms 27 | 28 | This subpackage contains a set of modules, each one of them containing some algorithms. 29 | """ 30 | -------------------------------------------------------------------------------- /pygraph/algorithms/accessibility.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2007-2009 Pedro Matiello 2 | # 3 | # Permission is hereby granted, free of charge, to any person 4 | # obtaining a copy of this software and associated documentation 5 | # files (the "Software"), to deal in the Software without 6 | # restriction, including without limitation the rights to use, 7 | # copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | # copies of the Software, and to permit persons to whom the 9 | # Software is furnished to do so, subject to the following 10 | # conditions: 11 | 12 | # The above copyright notice and this permission notice shall be 13 | # included in all copies or substantial portions of the Software. 14 | 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | # OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | 25 | """ 26 | Accessibility algorithms. 27 | 28 | @sort: accessibility, connected_components, cut_edges, cut_nodes, mutual_accessibility 29 | """ 30 | 31 | # Imports 32 | from sys import getrecursionlimit, setrecursionlimit 33 | 34 | # Transitive-closure 35 | 36 | 37 | def accessibility(graph): 38 | """ 39 | Accessibility matrix (transitive closure). 40 | 41 | @type graph: graph, digraph, hypergraph 42 | @param graph: Graph. 43 | 44 | @rtype: dictionary 45 | @return: Accessibility information for each node. 46 | """ 47 | recursionlimit = getrecursionlimit() 48 | setrecursionlimit(max(len(graph.nodes()) * 2, recursionlimit)) 49 | 50 | accessibility = {} # Accessibility matrix 51 | 52 | # For each node i, mark each node j if that exists a path from i to j. 53 | for each in graph: 54 | access = {} 55 | # Perform DFS to explore all reachable nodes 56 | _dfs(graph, access, 1, each) 57 | accessibility[each] = list(access.keys()) 58 | 59 | setrecursionlimit(recursionlimit) 60 | return accessibility 61 | 62 | 63 | # Strongly connected components 64 | 65 | 66 | def mutual_accessibility(graph): 67 | """ 68 | Mutual-accessibility matrix (strongly connected components). 69 | 70 | @type graph: graph, digraph 71 | @param graph: Graph. 72 | 73 | @rtype: dictionary 74 | @return: Mutual-accessibility information for each node. 75 | """ 76 | recursionlimit = getrecursionlimit() 77 | setrecursionlimit(max(len(graph.nodes()) * 2, recursionlimit)) 78 | 79 | mutual_access = {} 80 | stack = [] 81 | low = {} 82 | 83 | def visit(node): 84 | if node in low: 85 | return 86 | 87 | num = len(low) 88 | low[node] = num 89 | stack_pos = len(stack) 90 | stack.append(node) 91 | 92 | for successor in graph.neighbors(node): 93 | visit(successor) 94 | low[node] = min(low[node], low[successor]) 95 | 96 | if num == low[node]: 97 | component = stack[stack_pos:] 98 | del stack[stack_pos:] 99 | component.sort() 100 | for each in component: 101 | mutual_access[each] = component 102 | 103 | for item in component: 104 | low[item] = len(graph) 105 | 106 | for node in graph: 107 | visit(node) 108 | 109 | setrecursionlimit(recursionlimit) 110 | return mutual_access 111 | 112 | 113 | # Connected components 114 | 115 | 116 | def connected_components(graph): 117 | """ 118 | Connected components. 119 | 120 | @type graph: graph, hypergraph 121 | @param graph: Graph. 122 | 123 | @rtype: dictionary 124 | @return: Pairing that associates each node to its connected component. 125 | """ 126 | recursionlimit = getrecursionlimit() 127 | setrecursionlimit(max(len(graph.nodes()) * 2, recursionlimit)) 128 | 129 | visited = {} 130 | count = 1 131 | 132 | # For 'each' node not found to belong to a connected component, find its connected 133 | # component. 134 | for each in graph: 135 | if each not in visited: 136 | _dfs(graph, visited, count, each) 137 | count = count + 1 138 | 139 | setrecursionlimit(recursionlimit) 140 | return visited 141 | 142 | 143 | # Limited DFS implementations used by algorithms here 144 | 145 | 146 | def _dfs(graph, visited, count, node): 147 | """ 148 | Depth-first search subfunction adapted for accessibility algorithms. 149 | 150 | @type graph: graph, digraph, hypergraph 151 | @param graph: Graph. 152 | 153 | @type visited: dictionary 154 | @param visited: List of nodes (visited nodes are marked non-zero). 155 | 156 | @type count: number 157 | @param count: Counter of connected components. 158 | 159 | @type node: node 160 | @param node: Node to be explored by DFS. 161 | """ 162 | visited[node] = count 163 | # Explore recursively the connected component 164 | for each in graph[node]: 165 | if each not in visited: 166 | _dfs(graph, visited, count, each) 167 | 168 | 169 | # Cut-Edge and Cut-Vertex identification 170 | 171 | # This works by creating a spanning tree for the graph and keeping track of the preorder number 172 | # of each node in the graph in pre[]. The low[] number for each node tracks the pre[] number of 173 | # the node with lowest pre[] number reachable from the first node. 174 | # 175 | # An edge (u, v) will be a cut-edge low[u] == pre[v]. Suppose v under the spanning subtree with 176 | # root u. This means that, from u, through a path inside this subtree, followed by an backarc, 177 | # one can not get out the subtree. So, (u, v) is the only connection between this subtree and 178 | # the remaining parts of the graph and, when removed, will increase the number of connected 179 | # components. 180 | 181 | # Similarly, a node u will be a cut node if any of the nodes v in the spanning subtree rooted in 182 | # u are so that low[v] > pre[u], which means that there's no path from v to outside this subtree 183 | # without passing through u. 184 | 185 | 186 | def cut_edges(graph): 187 | """ 188 | Return the cut-edges of the given graph. 189 | 190 | A cut edge, or bridge, is an edge of a graph whose removal increases the number of connected 191 | components in the graph. 192 | 193 | @type graph: graph, hypergraph 194 | @param graph: Graph. 195 | 196 | @rtype: list 197 | @return: List of cut-edges. 198 | """ 199 | recursionlimit = getrecursionlimit() 200 | setrecursionlimit(max(len(graph.nodes()) * 2, recursionlimit)) 201 | 202 | # Dispatch if we have a hypergraph 203 | if "hypergraph" == graph.__class__.__name__: 204 | return _cut_hyperedges(graph) 205 | 206 | pre = {} # Pre-ordering 207 | low = {} # Lowest pre[] reachable from this node going down the spanning tree + one backedge 208 | spanning_tree = {} 209 | reply = [] 210 | pre[None] = 0 211 | 212 | for each in graph: 213 | if each not in pre: 214 | spanning_tree[each] = None 215 | _cut_dfs(graph, spanning_tree, pre, low, reply, each) 216 | 217 | setrecursionlimit(recursionlimit) 218 | return reply 219 | 220 | 221 | def _cut_hyperedges(hypergraph): 222 | """ 223 | Return the cut-hyperedges of the given hypergraph. 224 | 225 | @type hypergraph: hypergraph 226 | @param hypergraph: Hypergraph 227 | 228 | @rtype: list 229 | @return: List of cut-nodes. 230 | """ 231 | edges_ = cut_nodes(hypergraph.graph) 232 | edges = [] 233 | 234 | for each in edges_: 235 | if each[1] == "h": 236 | edges.append(each[0]) 237 | 238 | return edges 239 | 240 | 241 | def cut_nodes(graph): 242 | """ 243 | Return the cut-nodes of the given graph. 244 | 245 | A cut node, or articulation point, is a node of a graph whose removal increases the number of 246 | connected components in the graph. 247 | 248 | @type graph: graph, hypergraph 249 | @param graph: Graph. 250 | 251 | @rtype: list 252 | @return: List of cut-nodes. 253 | """ 254 | recursionlimit = getrecursionlimit() 255 | setrecursionlimit(max(len(graph.nodes()) * 2, recursionlimit)) 256 | 257 | # Dispatch if we have a hypergraph 258 | if "hypergraph" == graph.__class__.__name__: 259 | return _cut_hypernodes(graph) 260 | 261 | pre = {} # Pre-ordering 262 | low = {} # Lowest pre[] reachable from this node going down the spanning tree + one backedge 263 | reply = {} 264 | spanning_tree = {} 265 | pre[None] = 0 266 | 267 | # Create spanning trees, calculate pre[], low[] 268 | for each in graph: 269 | if each not in pre: 270 | spanning_tree[each] = None 271 | _cut_dfs(graph, spanning_tree, pre, low, [], each) 272 | 273 | # Find cuts 274 | for each in graph: 275 | # If node is not a root 276 | if spanning_tree[each] is not None: 277 | for other in graph[each]: 278 | # If there is no back-edge from descendent to a ancestral of each 279 | if low[other] >= pre[each] and spanning_tree[other] == each: 280 | reply[each] = 1 281 | # If node is a root 282 | else: 283 | children = 0 284 | for other in graph: 285 | if spanning_tree[other] == each: 286 | children = children + 1 287 | # root is cut-vertex iff it has two or more children 288 | if children >= 2: 289 | reply[each] = 1 290 | 291 | setrecursionlimit(recursionlimit) 292 | return list(reply.keys()) 293 | 294 | 295 | def _cut_hypernodes(hypergraph): 296 | """ 297 | Return the cut-nodes of the given hypergraph. 298 | 299 | @type hypergraph: hypergraph 300 | @param hypergraph: Hypergraph 301 | 302 | @rtype: list 303 | @return: List of cut-nodes. 304 | """ 305 | nodes_ = cut_nodes(hypergraph.graph) 306 | nodes = [] 307 | 308 | for each in nodes_: 309 | if each[1] == "n": 310 | nodes.append(each[0]) 311 | 312 | return nodes 313 | 314 | 315 | def _cut_dfs(graph, spanning_tree, pre, low, reply, node): 316 | """ 317 | Depth first search adapted for identification of cut-edges and cut-nodes. 318 | 319 | @type graph: graph, digraph 320 | @param graph: Graph 321 | 322 | @type spanning_tree: dictionary 323 | @param spanning_tree: Spanning tree being built for the graph by DFS. 324 | 325 | @type pre: dictionary 326 | @param pre: Graph's preordering. 327 | 328 | @type low: dictionary 329 | @param low: Associates to each node, the preordering index of the node of lowest preordering 330 | accessible from the given node. 331 | 332 | @type reply: list 333 | @param reply: List of cut-edges. 334 | 335 | @type node: node 336 | @param node: Node to be explored by DFS. 337 | """ 338 | pre[node] = pre[None] 339 | low[node] = pre[None] 340 | pre[None] = pre[None] + 1 341 | 342 | for each in graph[node]: 343 | if each not in pre: 344 | spanning_tree[each] = node 345 | _cut_dfs(graph, spanning_tree, pre, low, reply, each) 346 | if low[node] > low[each]: 347 | low[node] = low[each] 348 | if low[each] == pre[each]: 349 | reply.append((node, each)) 350 | elif low[node] > pre[each] and spanning_tree[node] != each: 351 | low[node] = pre[each] 352 | -------------------------------------------------------------------------------- /pygraph/algorithms/critical.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2009 Pedro Matiello 2 | # Tomaz Kovacic 3 | # 4 | # Permission is hereby granted, free of charge, to any person 5 | # obtaining a copy of this software and associated documentation 6 | # files (the "Software"), to deal in the Software without 7 | # restriction, including without limitation the rights to use, 8 | # copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the 10 | # Software is furnished to do so, subject to the following 11 | # conditions: 12 | 13 | # The above copyright notice and this permission notice shall be 14 | # included in all copies or substantial portions of the Software. 15 | 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 18 | # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 20 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 21 | # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 23 | # OTHER DEALINGS IN THE SOFTWARE. 24 | 25 | 26 | """ 27 | Critical path algorithms and transitivity detection algorithm. 28 | 29 | @sort: critical_path, transitive_edges 30 | """ 31 | 32 | # Imports 33 | from pygraph.algorithms.cycles import find_cycle 34 | from pygraph.algorithms.traversal import traversal 35 | from pygraph.algorithms.sorting import topological_sorting 36 | 37 | 38 | def _intersection(A, B): 39 | """ 40 | A simple function to find an intersection between two arrays. 41 | 42 | @type A: List 43 | @param A: First List 44 | 45 | @type B: List 46 | @param B: Second List 47 | 48 | @rtype: List 49 | @return: List of Intersections 50 | """ 51 | intersection = [] 52 | for i in A: 53 | if i in B: 54 | intersection.append(i) 55 | return intersection 56 | 57 | 58 | def transitive_edges(graph): 59 | """ 60 | Return a list of transitive edges. 61 | 62 | Example of transitivity within graphs: A -> B, B -> C, A -> C 63 | in this case the transitive edge is: A -> C 64 | 65 | @attention: This function is only meaningful for directed acyclic graphs. 66 | 67 | @type graph: digraph 68 | @param graph: Digraph 69 | 70 | @rtype: List 71 | @return: List containing tuples with transitive edges (or an empty array if the digraph 72 | contains a cycle) 73 | """ 74 | # if the graph contains a cycle we return an empty array 75 | if not len(find_cycle(graph)) == 0: 76 | return [] 77 | 78 | tranz_edges = [] # create an empty array that will contain all the tuples 79 | 80 | # run trough all the nodes in the graph 81 | for start in topological_sorting(graph): 82 | # find all the successors on the path for the current node 83 | successors = [] 84 | for a in traversal(graph, start, "pre"): 85 | successors.append(a) 86 | del successors[ 87 | 0 88 | ] # we need all the nodes in it's path except the start node itself 89 | 90 | for next in successors: 91 | # look for an intersection between all the neighbors of the 92 | # given node and all the neighbors from the given successor 93 | intersect_array = _intersection( 94 | graph.neighbors(next), graph.neighbors(start) 95 | ) 96 | for a in intersect_array: 97 | if graph.has_edge((start, a)): 98 | ##check for the detected edge and append it to the returned array 99 | tranz_edges.append((start, a)) 100 | return tranz_edges # return the final array 101 | 102 | 103 | def critical_path(graph): 104 | """ 105 | Compute and return the critical path in an acyclic directed weighted graph. 106 | 107 | @attention: This function is only meaningful for directed weighted acyclic graphs 108 | 109 | @type graph: digraph 110 | @param graph: Digraph 111 | 112 | @rtype: List 113 | @return: List containing all the nodes in the path (or an empty array if the graph 114 | contains a cycle) 115 | """ 116 | # if the graph contains a cycle we return an empty array 117 | if not len(find_cycle(graph)) == 0: 118 | return [] 119 | 120 | # this empty dictionary will contain a tuple for every single node 121 | # the tuple contains the information about the most costly predecessor 122 | # of the given node and the cost of the path to this node 123 | # (predecessor, cost) 124 | node_tuples = {} 125 | 126 | topological_nodes = topological_sorting(graph) 127 | 128 | # all the tuples must be set to a default value for every node in the graph 129 | for node in topological_nodes: 130 | node_tuples.update({node: (None, 0)}) 131 | 132 | # run trough all the nodes in a topological order 133 | for node in topological_nodes: 134 | predecessors = [] 135 | # we must check all the predecessors 136 | for pre in graph.incidents(node): 137 | max_pre = node_tuples[pre][1] 138 | predecessors.append((pre, graph.edge_weight((pre, node)) + max_pre)) 139 | 140 | max = 0 141 | max_tuple = (None, 0) 142 | for i in predecessors: # look for the most costly predecessor 143 | if i[1] >= max: 144 | max = i[1] 145 | max_tuple = i 146 | # assign the maximum value to the given node in the node_tuples dictionary 147 | node_tuples[node] = max_tuple 148 | 149 | # find the critical node 150 | max = 0 151 | critical_node = None 152 | for k, v in node_tuples.items(): 153 | if v[1] >= max: 154 | max = v[1] 155 | critical_node = k 156 | 157 | path = [] 158 | 159 | # find the critical path with backtracking trought the dictionary 160 | def mid_critical_path(end): 161 | if node_tuples[end][0] != None: 162 | path.append(end) 163 | mid_critical_path(node_tuples[end][0]) 164 | else: 165 | path.append(end) 166 | 167 | # call the recursive function 168 | mid_critical_path(critical_node) 169 | 170 | path.reverse() 171 | return path # return the array containing the critical path 172 | -------------------------------------------------------------------------------- /pygraph/algorithms/cycles.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2008-2009 Pedro Matiello 2 | # 3 | # Permission is hereby granted, free of charge, to any person 4 | # obtaining a copy of this software and associated documentation 5 | # files (the "Software"), to deal in the Software without 6 | # restriction, including without limitation the rights to use, 7 | # copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | # copies of the Software, and to permit persons to whom the 9 | # Software is furnished to do so, subject to the following 10 | # conditions: 11 | 12 | # The above copyright notice and this permission notice shall be 13 | # included in all copies or substantial portions of the Software. 14 | 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | # OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | 25 | """ 26 | Cycle detection algorithms. 27 | 28 | @sort: find_cycle 29 | """ 30 | 31 | # Imports 32 | from pygraph.classes.exceptions import InvalidGraphType 33 | from pygraph.classes.digraph import digraph as digraph_class 34 | from pygraph.classes.graph import graph as graph_class 35 | from sys import getrecursionlimit, setrecursionlimit 36 | 37 | 38 | def find_cycle(graph): 39 | """ 40 | Find a cycle in the given graph. 41 | 42 | This function will return a list of nodes which form a cycle in the graph or an empty list if 43 | no cycle exists. 44 | 45 | @type graph: graph, digraph 46 | @param graph: Graph. 47 | 48 | @rtype: list 49 | @return: List of nodes. 50 | """ 51 | 52 | if isinstance(graph, graph_class): 53 | directed = False 54 | elif isinstance(graph, digraph_class): 55 | directed = True 56 | else: 57 | raise InvalidGraphType 58 | 59 | def find_cycle_to_ancestor(node, ancestor): 60 | """ 61 | Find a cycle containing both node and ancestor. 62 | """ 63 | path = [] 64 | while node != ancestor: 65 | if node is None: 66 | return [] 67 | path.append(node) 68 | node = spanning_tree[node] 69 | path.append(node) 70 | path.reverse() 71 | return path 72 | 73 | def dfs(node): 74 | """ 75 | Depth-first search subfunction. 76 | """ 77 | visited[node] = 1 78 | # Explore recursively the connected component 79 | for each in graph[node]: 80 | if cycle: 81 | return 82 | if each not in visited: 83 | spanning_tree[each] = node 84 | dfs(each) 85 | else: 86 | if directed or spanning_tree[node] != each: 87 | cycle.extend(find_cycle_to_ancestor(node, each)) 88 | 89 | recursionlimit = getrecursionlimit() 90 | setrecursionlimit(max(len(graph.nodes()) * 2, recursionlimit)) 91 | 92 | visited = {} # List for marking visited and non-visited nodes 93 | spanning_tree = {} # Spanning tree 94 | cycle = [] 95 | 96 | # Algorithm outer-loop 97 | for each in graph: 98 | # Select a non-visited node 99 | if each not in visited: 100 | spanning_tree[each] = None 101 | # Explore node's connected component 102 | dfs(each) 103 | if cycle: 104 | setrecursionlimit(recursionlimit) 105 | return cycle 106 | 107 | setrecursionlimit(recursionlimit) 108 | return [] 109 | -------------------------------------------------------------------------------- /pygraph/algorithms/filters/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2008-2009 Pedro Matiello 2 | # 3 | # Permission is hereby granted, free of charge, to any person 4 | # obtaining a copy of this software and associated documentation 5 | # files (the "Software"), to deal in the Software without 6 | # restriction, including without limitation the rights to use, 7 | # copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | # copies of the Software, and to permit persons to whom the 9 | # Software is furnished to do so, subject to the following 10 | # conditions: 11 | 12 | # The above copyright notice and this permission notice shall be 13 | # included in all copies or substantial portions of the Software. 14 | 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | # OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | 25 | """ 26 | Set of searching filters. 27 | """ 28 | -------------------------------------------------------------------------------- /pygraph/algorithms/filters/find.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2008-2009 Pedro Matiello 2 | # 3 | # Permission is hereby granted, free of charge, to any person 4 | # obtaining a copy of this software and associated documentation 5 | # files (the "Software"), to deal in the Software without 6 | # restriction, including without limitation the rights to use, 7 | # copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | # copies of the Software, and to permit persons to whom the 9 | # Software is furnished to do so, subject to the following 10 | # conditions: 11 | 12 | # The above copyright notice and this permission notice shall be 13 | # included in all copies or substantial portions of the Software. 14 | 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | # OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | 25 | """ 26 | Search filter for finding a specific node. 27 | """ 28 | 29 | 30 | class find: 31 | """ 32 | Search filter for finding a specific node. 33 | """ 34 | 35 | def __init__(self, target): 36 | """ 37 | Initialize the filter. 38 | 39 | @type target: node 40 | @param target: Target node. 41 | """ 42 | self.graph = None 43 | self.spanning_tree = None 44 | self.target = target 45 | self.done = False 46 | 47 | def configure(self, graph, spanning_tree): 48 | """ 49 | Configure the filter. 50 | 51 | @type graph: graph 52 | @param graph: Graph. 53 | 54 | @type spanning_tree: dictionary 55 | @param spanning_tree: Spanning tree. 56 | """ 57 | self.graph = graph 58 | self.spanning_tree = spanning_tree 59 | 60 | def __call__(self, node, parent): 61 | """ 62 | Decide if the given node should be included in the search process. 63 | 64 | @type node: node 65 | @param node: Given node. 66 | 67 | @type parent: node 68 | @param parent: Given node's parent in the spanning tree. 69 | 70 | @rtype: boolean 71 | @return: Whether the given node should be included in the search process. 72 | """ 73 | if not self.done: 74 | if node == self.target: 75 | self.done = True 76 | return True 77 | else: 78 | return False 79 | -------------------------------------------------------------------------------- /pygraph/algorithms/filters/null.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2008-2009 Pedro Matiello 2 | # 3 | # Permission is hereby granted, free of charge, to any person 4 | # obtaining a copy of this software and associated documentation 5 | # files (the "Software"), to deal in the Software without 6 | # restriction, including without limitation the rights to use, 7 | # copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | # copies of the Software, and to permit persons to whom the 9 | # Software is furnished to do so, subject to the following 10 | # conditions: 11 | 12 | # The above copyright notice and this permission notice shall be 13 | # included in all copies or substantial portions of the Software. 14 | 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | # OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | 25 | """ 26 | Null searching filter. 27 | """ 28 | 29 | 30 | class null: 31 | """ 32 | Null search filter. 33 | """ 34 | 35 | def __init__(self): 36 | """ 37 | Initialize the filter. 38 | """ 39 | self.graph = None 40 | self.spanning_tree = None 41 | 42 | def configure(self, graph, spanning_tree): 43 | """ 44 | Configure the filter. 45 | 46 | @type graph: graph 47 | @param graph: Graph. 48 | 49 | @type spanning_tree: dictionary 50 | @param spanning_tree: Spanning tree. 51 | """ 52 | self.graph = graph 53 | self.spanning_tree = spanning_tree 54 | 55 | def __call__(self, node, parent): 56 | """ 57 | Decide if the given node should be included in the search process. 58 | 59 | @type node: node 60 | @param node: Given node. 61 | 62 | @type parent: node 63 | @param parent: Given node's parent in the spanning tree. 64 | 65 | @rtype: boolean 66 | @return: Whether the given node should be included in the search process. 67 | """ 68 | return True 69 | -------------------------------------------------------------------------------- /pygraph/algorithms/filters/radius.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2008-2009 Pedro Matiello 2 | # 3 | # Permission is hereby granted, free of charge, to any person 4 | # obtaining a copy of this software and associated documentation 5 | # files (the "Software"), to deal in the Software without 6 | # restriction, including without limitation the rights to use, 7 | # copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | # copies of the Software, and to permit persons to whom the 9 | # Software is furnished to do so, subject to the following 10 | # conditions: 11 | 12 | # The above copyright notice and this permission notice shall be 13 | # included in all copies or substantial portions of the Software. 14 | 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | # OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | 25 | """ 26 | Radial search filter. 27 | """ 28 | 29 | 30 | class radius: 31 | """ 32 | Radial search filter. 33 | 34 | This will keep searching contained inside a specified limit. 35 | """ 36 | 37 | def __init__(self, radius): 38 | """ 39 | Initialize the filter. 40 | 41 | @type radius: number 42 | @param radius: Search radius. 43 | """ 44 | self.graph = None 45 | self.spanning_tree = None 46 | self.radius = radius 47 | self.done = False 48 | 49 | def configure(self, graph, spanning_tree): 50 | """ 51 | Configure the filter. 52 | 53 | @type graph: graph 54 | @param graph: Graph. 55 | 56 | @type spanning_tree: dictionary 57 | @param spanning_tree: Spanning tree. 58 | """ 59 | self.graph = graph 60 | self.spanning_tree = spanning_tree 61 | 62 | def __call__(self, node, parent): 63 | """ 64 | Decide if the given node should be included in the search process. 65 | 66 | @type node: node 67 | @param node: Given node. 68 | 69 | @type parent: node 70 | @param parent: Given node's parent in the spanning tree. 71 | 72 | @rtype: boolean 73 | @return: Whether the given node should be included in the search process. 74 | """ 75 | 76 | def cost_to_root(node): 77 | if node is not None: 78 | return cost_to_parent(node, st[node]) + cost_to_root(st[node]) 79 | else: 80 | return 0 81 | 82 | def cost_to_parent(node, parent): 83 | if parent is not None: 84 | return gr.edge_weight((parent, node)) 85 | else: 86 | return 0 87 | 88 | gr = self.graph 89 | st = self.spanning_tree 90 | 91 | cost = cost_to_parent(node, parent) + cost_to_root(parent) 92 | 93 | if cost <= self.radius: 94 | return True 95 | else: 96 | return False 97 | -------------------------------------------------------------------------------- /pygraph/algorithms/generators.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2008-2009 Pedro Matiello 2 | # Zsolt Haraszti 3 | # 4 | # Permission is hereby granted, free of charge, to any person 5 | # obtaining a copy of this software and associated documentation 6 | # files (the "Software"), to deal in the Software without 7 | # restriction, including without limitation the rights to use, 8 | # copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the 10 | # Software is furnished to do so, subject to the following 11 | # conditions: 12 | 13 | # The above copyright notice and this permission notice shall be 14 | # included in all copies or substantial portions of the Software. 15 | 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 18 | # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 20 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 21 | # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 23 | # OTHER DEALINGS IN THE SOFTWARE. 24 | 25 | 26 | """ 27 | Random graph generators. 28 | 29 | @sort: generate, generate_hypergraph 30 | """ 31 | 32 | # Imports 33 | from pygraph.classes.graph import graph 34 | from pygraph.classes.digraph import digraph 35 | from pygraph.classes.hypergraph import hypergraph 36 | from random import randint, choice, shuffle # @UnusedImport 37 | 38 | # Generator 39 | 40 | 41 | def generate(num_nodes, num_edges, directed=False, weight_range=(1, 1)): 42 | """ 43 | Create a random graph. 44 | 45 | @type num_nodes: number 46 | @param num_nodes: Number of nodes. 47 | 48 | @type num_edges: number 49 | @param num_edges: Number of edges. 50 | 51 | @type directed: bool 52 | @param directed: Whether the generated graph should be directed or not. 53 | 54 | @type weight_range: tuple 55 | @param weight_range: tuple of two integers as lower and upper limits on randomly generated 56 | weights (uniform distribution). 57 | """ 58 | # Graph creation 59 | if directed: 60 | random_graph = digraph() 61 | else: 62 | random_graph = graph() 63 | 64 | # Nodes 65 | nodes = list(range(num_nodes)) 66 | random_graph.add_nodes(nodes) 67 | 68 | # Build a list of all possible edges 69 | edges = [] 70 | edges_append = edges.append 71 | for x in nodes: 72 | for y in nodes: 73 | if (directed and x != y) or (x > y): 74 | edges_append((x, y)) 75 | 76 | # Randomize the list 77 | shuffle(edges) 78 | 79 | # Add edges to the graph 80 | min_wt = min(weight_range) 81 | max_wt = max(weight_range) 82 | for i in range(num_edges): 83 | each = edges[i] 84 | random_graph.add_edge((each[0], each[1]), wt=randint(min_wt, max_wt)) 85 | 86 | return random_graph 87 | 88 | 89 | def generate_hypergraph(num_nodes, num_edges, r=0): 90 | """ 91 | Create a random hyper graph. 92 | 93 | @type num_nodes: number 94 | @param num_nodes: Number of nodes. 95 | 96 | @type num_edges: number 97 | @param num_edges: Number of edges. 98 | 99 | @type r: number 100 | @param r: Uniform edges of size r. 101 | """ 102 | # Graph creation 103 | random_graph = hypergraph() 104 | 105 | # Nodes 106 | nodes = list(map(str, range(num_nodes))) 107 | random_graph.add_nodes(nodes) 108 | 109 | # Base edges 110 | edges = list(map(str, range(num_nodes, num_nodes + num_edges))) 111 | random_graph.add_hyperedges(edges) 112 | 113 | # Connect the edges 114 | if 0 == r: 115 | # Add each edge with 50/50 probability 116 | for e in edges: 117 | for n in nodes: 118 | if choice([True, False]): 119 | random_graph.link(n, e) 120 | 121 | else: 122 | # Add only uniform edges 123 | for e in edges: 124 | # First shuffle the nodes 125 | shuffle(nodes) 126 | 127 | # Then take the first r nodes 128 | for i in range(r): 129 | random_graph.link(nodes[i], e) 130 | 131 | return random_graph 132 | -------------------------------------------------------------------------------- /pygraph/algorithms/heuristics/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2008-2009 Pedro Matiello 2 | # Salim Fadhley 3 | # 4 | # Permission is hereby granted, free of charge, to any person 5 | # obtaining a copy of this software and associated documentation 6 | # files (the "Software"), to deal in the Software without 7 | # restriction, including without limitation the rights to use, 8 | # copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the 10 | # Software is furnished to do so, subject to the following 11 | # conditions: 12 | 13 | # The above copyright notice and this permission notice shall be 14 | # included in all copies or substantial portions of the Software. 15 | 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 18 | # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 20 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 21 | # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 23 | # OTHER DEALINGS IN THE SOFTWARE. 24 | 25 | 26 | """ 27 | Set of search heuristics. 28 | 29 | These are to be used with the C{heuristic_search()} function. 30 | """ 31 | -------------------------------------------------------------------------------- /pygraph/algorithms/heuristics/chow.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2008-2009 Pedro Matiello 2 | # Salim Fadhley 3 | # 4 | # Permission is hereby granted, free of charge, to any person 5 | # obtaining a copy of this software and associated documentation 6 | # files (the "Software"), to deal in the Software without 7 | # restriction, including without limitation the rights to use, 8 | # copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the 10 | # Software is furnished to do so, subject to the following 11 | # conditions: 12 | 13 | # The above copyright notice and this permission notice shall be 14 | # included in all copies or substantial portions of the Software. 15 | 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 18 | # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 20 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 21 | # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 23 | # OTHER DEALINGS IN THE SOFTWARE. 24 | 25 | 26 | """ 27 | Edmond Chow's heuristic for A*. 28 | """ 29 | 30 | # Imports 31 | from pygraph.algorithms.minmax import shortest_path 32 | 33 | 34 | class chow: 35 | """ 36 | An implementation of the graph searching heuristic proposed by Edmond Chow. 37 | 38 | Remember to call the C{optimize()} method before the heuristic search. 39 | 40 | For details, check: U{http://www.edmondchow.com/pubs/levdiff-aaai.pdf}. 41 | """ 42 | 43 | def __init__(self, *centers): 44 | """ 45 | Initialize a Chow heuristic object. 46 | """ 47 | self.centers = centers 48 | self.nodes = {} 49 | 50 | def optimize(self, graph): 51 | """ 52 | Build a dictionary mapping each pair of nodes to a number (the distance between them). 53 | 54 | @type graph: graph 55 | @param graph: Graph. 56 | """ 57 | for center in self.centers: 58 | shortest_routes = shortest_path(graph, center)[1] 59 | for node, weight in shortest_routes.items(): 60 | self.nodes.setdefault(node, []).append(weight) 61 | 62 | def __call__(self, start, end): 63 | """ 64 | Estimate how far start is from end. 65 | 66 | @type start: node 67 | @param start: Start node. 68 | 69 | @type end: node 70 | @param end: End node. 71 | """ 72 | assert len(self.nodes.keys()) > 0, ( 73 | "You need to optimize this heuristic for your graph before it can be used to estimate." 74 | ) 75 | 76 | cmp_sequence = zip(self.nodes[start], self.nodes[end]) 77 | chow_number = max(abs(a - b) for a, b in cmp_sequence) 78 | return chow_number 79 | -------------------------------------------------------------------------------- /pygraph/algorithms/heuristics/euclidean.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2008-2009 Pedro Matiello 2 | # 3 | # Permission is hereby granted, free of charge, to any person 4 | # obtaining a copy of this software and associated documentation 5 | # files (the "Software"), to deal in the Software without 6 | # restriction, including without limitation the rights to use, 7 | # copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | # copies of the Software, and to permit persons to whom the 9 | # Software is furnished to do so, subject to the following 10 | # conditions: 11 | 12 | # The above copyright notice and this permission notice shall be 13 | # included in all copies or substantial portions of the Software. 14 | 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | # OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | 25 | """ 26 | A* heuristic for euclidean graphs. 27 | """ 28 | 29 | 30 | # Imports 31 | 32 | 33 | class euclidean: 34 | """ 35 | A* heuristic for Euclidean graphs. 36 | 37 | This heuristic has three requirements: 38 | 1. All nodes should have the attribute 'position'; 39 | 2. The weight of all edges should be the euclidean distance between the nodes it links; 40 | 3. The C{optimize()} method should be called before the heuristic search. 41 | 42 | A small example for clarification: 43 | >>> from pygraph.classes import graph 44 | >>> g = graph.graph() 45 | >>> g.add_nodes(['A','B','C']) 46 | >>> g.add_node_attribute('A', ('position',(0,0))) 47 | >>> g.add_node_attribute('B', ('position',(1,1))) 48 | >>> g.add_node_attribute('C', ('position',(0,2))) 49 | >>> g.add_edge(('A','B'), wt=2) 50 | >>> g.add_edge(('B','C'), wt=2) 51 | >>> g.add_edge(('A','C'), wt=4) 52 | >>> h = euclidean() 53 | >>> h.optimize(g) 54 | >>> from pygraph.algorithms.minmax import heuristic_search 55 | >>> heuristic_search(g, 'A', 'C', h) 56 | ['A', 'C'] 57 | """ 58 | 59 | def __init__(self): 60 | """ 61 | Initialize the heuristic object. 62 | """ 63 | self.distances = {} 64 | 65 | def optimize(self, graph): 66 | """ 67 | Build a dictionary mapping each pair of nodes to a number (the distance between them). 68 | 69 | @type graph: graph 70 | @param graph: Graph. 71 | """ 72 | for start in graph.nodes(): 73 | for end in graph.nodes(): 74 | for each in graph.node_attributes(start): 75 | if each[0] == "position": 76 | start_attr = each[1] 77 | break 78 | for each in graph.node_attributes(end): 79 | if each[0] == "position": 80 | end_attr = each[1] 81 | break 82 | dist = 0 83 | for i in range(len(start_attr)): 84 | dist = dist + (float(start_attr[i]) - float(end_attr[i])) ** 2 85 | self.distances[(start, end)] = dist 86 | 87 | def __call__(self, start, end): 88 | """ 89 | Estimate how far start is from end. 90 | 91 | @type start: node 92 | @param start: Start node. 93 | 94 | @type end: node 95 | @param end: End node. 96 | """ 97 | assert len(self.distances.keys()) > 0, ( 98 | "You need to optimize this heuristic for your graph before it can be used to estimate." 99 | ) 100 | 101 | return self.distances[(start, end)] 102 | -------------------------------------------------------------------------------- /pygraph/algorithms/pagerank.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2010 Pedro Matiello 2 | # Juarez Bochi 3 | # 4 | # Permission is hereby granted, free of charge, to any person 5 | # obtaining a copy of this software and associated documentation 6 | # files (the "Software"), to deal in the Software without 7 | # restriction, including without limitation the rights to use, 8 | # copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the 10 | # Software is furnished to do so, subject to the following 11 | # conditions: 12 | 13 | # The above copyright notice and this permission notice shall be 14 | # included in all copies or substantial portions of the Software. 15 | 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 18 | # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 20 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 21 | # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 23 | # OTHER DEALINGS IN THE SOFTWARE. 24 | 25 | 26 | """ 27 | PageRank algoritm 28 | 29 | @sort: pagerank 30 | """ 31 | 32 | 33 | def pagerank(graph, damping_factor=0.85, max_iterations=100, min_delta=0.00001): 34 | """ 35 | Compute and return the PageRank in an directed graph. 36 | 37 | @type graph: digraph 38 | @param graph: Digraph. 39 | 40 | @type damping_factor: number 41 | @param damping_factor: PageRank dumping factor. 42 | 43 | @type max_iterations: number 44 | @param max_iterations: Maximum number of iterations. 45 | 46 | @type min_delta: number 47 | @param min_delta: Smallest variation required to have a new iteration. 48 | 49 | @rtype: Dict 50 | @return: Dict containing all the nodes PageRank. 51 | """ 52 | 53 | nodes = graph.nodes() 54 | graph_size = len(nodes) 55 | if graph_size == 0: 56 | return {} 57 | min_value = ( 58 | 1.0 - damping_factor 59 | ) / graph_size # value for nodes without inbound links 60 | 61 | # itialize the page rank dict with 1/N for all nodes 62 | pagerank = dict.fromkeys(nodes, 1.0 / graph_size) 63 | 64 | for i in range(max_iterations): 65 | diff = 0 # total difference compared to last iteraction 66 | # computes each node PageRank based on inbound links 67 | for node in nodes: 68 | rank = min_value 69 | for referring_page in graph.incidents(node): 70 | rank += ( 71 | damping_factor 72 | * pagerank[referring_page] 73 | / len(graph.neighbors(referring_page)) 74 | ) 75 | 76 | diff += abs(pagerank[node] - rank) 77 | pagerank[node] = rank 78 | 79 | # stop if PageRank has converged 80 | if diff < min_delta: 81 | break 82 | 83 | return pagerank 84 | -------------------------------------------------------------------------------- /pygraph/algorithms/searching.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2007-2009 Pedro Matiello 2 | # 3 | # Permission is hereby granted, free of charge, to any person 4 | # obtaining a copy of this software and associated documentation 5 | # files (the "Software"), to deal in the Software without 6 | # restriction, including without limitation the rights to use, 7 | # copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | # copies of the Software, and to permit persons to whom the 9 | # Software is furnished to do so, subject to the following 10 | # conditions: 11 | 12 | # The above copyright notice and this permission notice shall be 13 | # included in all copies or substantial portions of the Software. 14 | 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | # OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | 25 | """ 26 | Search algorithms. 27 | 28 | @sort: breadth_first_search, depth_first_search 29 | """ 30 | 31 | # Imports 32 | from pygraph.algorithms.filters.null import null 33 | from sys import getrecursionlimit, setrecursionlimit 34 | 35 | 36 | # Depth-first search 37 | 38 | 39 | def depth_first_search(graph, root=None, filter=null()): 40 | """ 41 | Depth-first search. 42 | 43 | @type graph: graph, digraph 44 | @param graph: Graph. 45 | 46 | @type root: node 47 | @param root: Optional root node (will explore only root's connected component) 48 | 49 | @rtype: tuple 50 | @return: A tupple containing a dictionary and two lists: 51 | 1. Generated spanning tree 52 | 2. Graph's preordering 53 | 3. Graph's postordering 54 | """ 55 | 56 | recursionlimit = getrecursionlimit() 57 | setrecursionlimit(max(len(graph.nodes()) * 2, recursionlimit)) 58 | 59 | def dfs(node): 60 | """ 61 | Depth-first search subfunction. 62 | """ 63 | visited[node] = 1 64 | pre.append(node) 65 | # Explore recursively the connected component 66 | for each in graph[node]: 67 | if each not in visited and filter(each, node): 68 | spanning_tree[each] = node 69 | dfs(each) 70 | post.append(node) 71 | 72 | visited = {} # List for marking visited and non-visited nodes 73 | spanning_tree = {} # Spanning tree 74 | pre = [] # Graph's preordering 75 | post = [] # Graph's postordering 76 | filter.configure(graph, spanning_tree) 77 | 78 | # DFS from one node only 79 | if root is not None: 80 | if filter(root, None): 81 | spanning_tree[root] = None 82 | dfs(root) 83 | setrecursionlimit(recursionlimit) 84 | return spanning_tree, pre, post 85 | 86 | # Algorithm loop 87 | for each in graph: 88 | # Select a non-visited node 89 | if each not in visited and filter(each, None): 90 | spanning_tree[each] = None 91 | # Explore node's connected component 92 | dfs(each) 93 | 94 | setrecursionlimit(recursionlimit) 95 | 96 | return (spanning_tree, pre, post) 97 | 98 | 99 | # Breadth-first search 100 | 101 | 102 | def breadth_first_search(graph, root=None, filter=null()): 103 | """ 104 | Breadth-first search. 105 | 106 | @type graph: graph, digraph 107 | @param graph: Graph. 108 | 109 | @type root: node 110 | @param root: Optional root node (will explore only root's connected component) 111 | 112 | @rtype: tuple 113 | @return: A tuple containing a dictionary and a list. 114 | 1. Generated spanning tree 115 | 2. Graph's level-based ordering 116 | """ 117 | 118 | def bfs(): 119 | """ 120 | Breadth-first search subfunction. 121 | """ 122 | while queue != []: 123 | node = queue.pop(0) 124 | 125 | for other in graph[node]: 126 | if other not in spanning_tree and filter(other, node): 127 | queue.append(other) 128 | ordering.append(other) 129 | spanning_tree[other] = node 130 | 131 | queue = [] # Visiting queue 132 | spanning_tree = {} # Spanning tree 133 | ordering = [] 134 | filter.configure(graph, spanning_tree) 135 | 136 | # BFS from one node only 137 | if root is not None: 138 | if filter(root, None): 139 | queue.append(root) 140 | ordering.append(root) 141 | spanning_tree[root] = None 142 | bfs() 143 | return spanning_tree, ordering 144 | 145 | # Algorithm 146 | for each in graph: 147 | if each not in spanning_tree: 148 | if filter(each, None): 149 | queue.append(each) 150 | ordering.append(each) 151 | spanning_tree[each] = None 152 | bfs() 153 | 154 | return spanning_tree, ordering 155 | -------------------------------------------------------------------------------- /pygraph/algorithms/sorting.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2007-2009 Pedro Matiello 2 | # 3 | # Permission is hereby granted, free of charge, to any person 4 | # obtaining a copy of this software and associated documentation 5 | # files (the "Software"), to deal in the Software without 6 | # restriction, including without limitation the rights to use, 7 | # copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | # copies of the Software, and to permit persons to whom the 9 | # Software is furnished to do so, subject to the following 10 | # conditions: 11 | 12 | # The above copyright notice and this permission notice shall be 13 | # included in all copies or substantial portions of the Software. 14 | 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | # OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | 25 | """ 26 | Sorting algorithms. 27 | 28 | @sort: topological_sorting 29 | """ 30 | 31 | # Imports 32 | from pygraph.algorithms.searching import depth_first_search 33 | 34 | 35 | # Topological sorting 36 | def topological_sorting(graph): 37 | """ 38 | Topological sorting. 39 | 40 | @attention: Topological sorting is meaningful only for directed acyclic graphs. 41 | 42 | @type graph: digraph 43 | @param graph: Graph. 44 | 45 | @rtype: list 46 | @return: Topological sorting for the graph. 47 | """ 48 | # The topological sorting of a DAG is equivalent to its reverse postordering. 49 | order = depth_first_search(graph)[2] 50 | order.reverse() 51 | return order 52 | -------------------------------------------------------------------------------- /pygraph/algorithms/traversal.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2008-2009 Pedro Matiello 2 | # 3 | # Permission is hereby granted, free of charge, to any person 4 | # obtaining a copy of this software and associated documentation 5 | # files (the "Software"), to deal in the Software without 6 | # restriction, including without limitation the rights to use, 7 | # copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | # copies of the Software, and to permit persons to whom the 9 | # Software is furnished to do so, subject to the following 10 | # conditions: 11 | 12 | # The above copyright notice and this permission notice shall be 13 | # included in all copies or substantial portions of the Software. 14 | 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | # OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | 25 | """ 26 | Traversal algorithms. 27 | 28 | @sort: traversal 29 | """ 30 | 31 | 32 | # Minimal spanning tree 33 | 34 | 35 | def traversal(graph, node, order): 36 | """ 37 | Graph traversal iterator. 38 | 39 | @type graph: graph, digraph 40 | @param graph: Graph. 41 | 42 | @type node: node 43 | @param node: Node. 44 | 45 | @type order: string 46 | @param order: traversal ordering. Possible values are: 47 | 2. 'pre' - Preordering (default) 48 | 1. 'post' - Postordering 49 | 50 | @rtype: iterator 51 | @return: Traversal iterator. 52 | """ 53 | visited = {} 54 | if order == "pre": 55 | pre = 1 56 | post = 0 57 | elif order == "post": 58 | pre = 0 59 | post = 1 60 | 61 | for each in _dfs(graph, visited, node, pre, post): 62 | yield each 63 | 64 | 65 | def _dfs(graph, visited, node, pre, post): 66 | """ 67 | Depth-first search subfunction for traversals. 68 | 69 | @type graph: graph, digraph 70 | @param graph: Graph. 71 | 72 | @type visited: dictionary 73 | @param visited: List of nodes (visited nodes are marked non-zero). 74 | 75 | @type node: node 76 | @param node: Node to be explored by DFS. 77 | """ 78 | visited[node] = 1 79 | if pre: 80 | yield node 81 | # Explore recursively the connected component 82 | for each in graph[node]: 83 | if each not in visited: 84 | for other in _dfs(graph, visited, each, pre, post): 85 | yield other 86 | if post: 87 | yield node 88 | -------------------------------------------------------------------------------- /pygraph/algorithms/utils.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2008-2009 Pedro Matiello 2 | # Roy Smith 3 | # Salim Fadhley 4 | # 5 | # Permission is hereby granted, free of charge, to any person 6 | # obtaining a copy of this software and associated documentation 7 | # files (the "Software"), to deal in the Software without 8 | # restriction, including without limitation the rights to use, 9 | # copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | # copies of the Software, and to permit persons to whom the 11 | # Software is furnished to do so, subject to the following 12 | # conditions: 13 | 14 | # The above copyright notice and this permission notice shall be 15 | # included in all copies or substantial portions of the Software. 16 | 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 19 | # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 21 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 22 | # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 23 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 24 | # OTHER DEALINGS IN THE SOFTWARE. 25 | 26 | 27 | """ 28 | Miscellaneous useful stuff. 29 | """ 30 | 31 | # Imports 32 | from heapq import heappush, heappop, heapify 33 | 34 | 35 | # Priority Queue 36 | class priority_queue: 37 | """ 38 | Priority queue. 39 | """ 40 | 41 | def __init__(self, list=[]): 42 | self.heap = [HeapItem(i, 0) for i in list] 43 | heapify(self.heap) 44 | 45 | def __contains__(self, item): 46 | for heap_item in self.heap: 47 | if item == heap_item.item: 48 | return True 49 | return False 50 | 51 | def __len__(self): 52 | return len(self.heap) 53 | 54 | def empty(self): 55 | return len(self.heap) == 0 56 | 57 | def insert(self, item, priority): 58 | """ 59 | Insert item into the queue, with the given priority. 60 | """ 61 | heappush(self.heap, HeapItem(item, priority)) 62 | 63 | def pop(self): 64 | """ 65 | Return the item with the lowest priority, and remove it from the queue. 66 | """ 67 | return heappop(self.heap).item 68 | 69 | def peek(self): 70 | """ 71 | Return the item with the lowest priority. The queue is unchanged. 72 | """ 73 | return self.heap[0].item 74 | 75 | def discard(self, item): 76 | new_heap = [] 77 | for heap_item in self.heap: 78 | if item != heap_item.item: 79 | new_heap.append(heap_item) 80 | self.heap = new_heap 81 | heapify(self.heap) 82 | 83 | 84 | class HeapItem: 85 | def __init__(self, item, priority): 86 | self.item = item 87 | self.priority = priority 88 | 89 | def __cmp__(self, other): 90 | return (self.priority > other.priority) - (self.priority < other.priority) 91 | -------------------------------------------------------------------------------- /pygraph/classes/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2008-2009 Pedro Matiello 2 | # 3 | # Permission is hereby granted, free of charge, to any person 4 | # obtaining a copy of this software and associated documentation 5 | # files (the "Software"), to deal in the Software without 6 | # restriction, including without limitation the rights to use, 7 | # copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | # copies of the Software, and to permit persons to whom the 9 | # Software is furnished to do so, subject to the following 10 | # conditions: 11 | 12 | # The above copyright notice and this permission notice shall be 13 | # included in all copies or substantial portions of the Software. 14 | 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | # OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | 25 | """ 26 | Data structure classes. 27 | """ 28 | -------------------------------------------------------------------------------- /pygraph/classes/digraph.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2007-2009 Pedro Matiello 2 | # Christian Muise 3 | # Johannes Reinhardt 4 | # Nathan Davis 5 | # Zsolt Haraszti 6 | # 7 | # Permission is hereby granted, free of charge, to any person 8 | # obtaining a copy of this software and associated documentation 9 | # files (the "Software"), to deal in the Software without 10 | # restriction, including without limitation the rights to use, 11 | # copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | # copies of the Software, and to permit persons to whom the 13 | # Software is furnished to do so, subject to the following 14 | # conditions: 15 | 16 | # The above copyright notice and this permission notice shall be 17 | # included in all copies or substantial portions of the Software. 18 | 19 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 20 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 21 | # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 22 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 23 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 24 | # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 25 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 26 | # OTHER DEALINGS IN THE SOFTWARE. 27 | """ 28 | Digraph class 29 | """ 30 | 31 | # Imports 32 | from pygraph.classes.exceptions import AdditionError 33 | from pygraph.mixins.labeling import labeling 34 | from pygraph.mixins.common import common 35 | from pygraph.mixins.basegraph import basegraph 36 | 37 | 38 | class digraph(basegraph, common, labeling): 39 | """ 40 | Digraph class. 41 | 42 | Digraphs are built of nodes and directed edges. 43 | 44 | @sort: __eq__, __init__, __ne__, add_edge, add_node, del_edge, del_node, edges, has_edge, has_node, 45 | incidents, neighbors, node_order, nodes 46 | """ 47 | 48 | DIRECTED = True 49 | 50 | def __init__(self): 51 | """ 52 | Initialize a digraph. 53 | """ 54 | common.__init__(self) 55 | labeling.__init__(self) 56 | self.node_neighbors = {} # Pairing: Node -> Neighbors 57 | self.node_incidence = {} # Pairing: Node -> Incident nodes 58 | 59 | def nodes(self): 60 | """ 61 | Return node list. 62 | 63 | @rtype: list 64 | @return: Node list. 65 | """ 66 | return list(self.node_neighbors.keys()) 67 | 68 | def neighbors(self, node): 69 | """ 70 | Return all nodes that are directly accessible from given node. 71 | 72 | @type node: node 73 | @param node: Node identifier 74 | 75 | @rtype: list 76 | @return: List of nodes directly accessible from given node. 77 | """ 78 | return self.node_neighbors[node] 79 | 80 | def incidents(self, node): 81 | """ 82 | Return all nodes that are incident to the given node. 83 | 84 | @type node: node 85 | @param node: Node identifier 86 | 87 | @rtype: list 88 | @return: List of nodes directly accessible from given node. 89 | """ 90 | return self.node_incidence[node] 91 | 92 | def edges(self): 93 | """ 94 | Return all edges in the graph. 95 | 96 | @rtype: list 97 | @return: List of all edges in the graph. 98 | """ 99 | return list(self._edges()) 100 | 101 | def _edges(self): 102 | for n, neighbors in self.node_neighbors.items(): 103 | for neighbor in neighbors: 104 | yield n, neighbor 105 | 106 | def has_node(self, node): 107 | """ 108 | Return whether the requested node exists. 109 | 110 | @type node: node 111 | @param node: Node identifier 112 | 113 | @rtype: boolean 114 | @return: Truth-value for node existence. 115 | """ 116 | return node in self.node_neighbors 117 | 118 | def add_node(self, node, attrs=None): 119 | """ 120 | Add given node to the graph. 121 | 122 | @attention: While nodes can be of any type, it's strongly recommended to use only 123 | numbers and single-line strings as node identifiers if you intend to use write(). 124 | 125 | @type node: node 126 | @param node: Node identifier. 127 | 128 | @type attrs: list 129 | @param attrs: List of node attributes specified as (attribute, value) tuples. 130 | """ 131 | if attrs is None: 132 | attrs = [] 133 | if node not in self.node_neighbors: 134 | self.node_neighbors[node] = [] 135 | self.node_incidence[node] = [] 136 | self.node_attr[node] = attrs 137 | else: 138 | raise AdditionError("Node %s already in digraph" % node) 139 | 140 | def add_edge(self, edge, wt=1, label="", attrs=[]): 141 | """ 142 | Add an directed edge to the graph connecting two nodes. 143 | 144 | An edge, here, is a pair of nodes like C{(n, m)}. 145 | 146 | @type edge: tuple 147 | @param edge: Edge. 148 | 149 | @type wt: number 150 | @param wt: Edge weight. 151 | 152 | @type label: string 153 | @param label: Edge label. 154 | 155 | @type attrs: list 156 | @param attrs: List of node attributes specified as (attribute, value) tuples. 157 | """ 158 | u, v = edge 159 | for n in [u, v]: 160 | if n not in self.node_neighbors: 161 | raise AdditionError("%s is missing from the node_neighbors table" % n) 162 | if n not in self.node_incidence: 163 | raise AdditionError("%s is missing from the node_incidence table" % n) 164 | 165 | if v in self.node_neighbors[u] and u in self.node_incidence[v]: 166 | raise AdditionError("Edge (%s, %s) already in digraph" % (u, v)) 167 | else: 168 | self.node_neighbors[u].append(v) 169 | self.node_incidence[v].append(u) 170 | self.set_edge_weight((u, v), wt) 171 | self.add_edge_attributes((u, v), attrs) 172 | self.set_edge_properties((u, v), label=label, weight=wt) 173 | 174 | def del_node(self, node): 175 | """ 176 | Remove a node from the graph. 177 | 178 | @type node: node 179 | @param node: Node identifier. 180 | """ 181 | for each in list(self.incidents(node)): 182 | # Delete all the edges incident on this node 183 | self.del_edge((each, node)) 184 | 185 | for each in list(self.neighbors(node)): 186 | # Delete all the edges pointing to this node. 187 | self.del_edge((node, each)) 188 | 189 | # Remove this node from the neighbors and incidents tables 190 | del self.node_neighbors[node] 191 | del self.node_incidence[node] 192 | 193 | # Remove any labeling which may exist. 194 | self.del_node_labeling(node) 195 | 196 | def del_edge(self, edge): 197 | """ 198 | Remove an directed edge from the graph. 199 | 200 | @type edge: tuple 201 | @param edge: Edge. 202 | """ 203 | u, v = edge 204 | self.node_neighbors[u].remove(v) 205 | self.node_incidence[v].remove(u) 206 | self.del_edge_labeling((u, v)) 207 | 208 | def has_edge(self, edge): 209 | """ 210 | Return whether an edge exists. 211 | 212 | @type edge: tuple 213 | @param edge: Edge. 214 | 215 | @rtype: boolean 216 | @return: Truth-value for edge existence. 217 | """ 218 | u, v = edge 219 | return (u, v) in self.edge_properties 220 | 221 | def node_order(self, node): 222 | """ 223 | Return the order of the given node. 224 | 225 | @rtype: number 226 | @return: Order of the given node. 227 | """ 228 | return len(self.neighbors(node)) 229 | 230 | def __eq__(self, other): 231 | """ 232 | Return whether this graph is equal to another one. 233 | 234 | @type other: graph, digraph 235 | @param other: Other graph or digraph 236 | 237 | @rtype: boolean 238 | @return: Whether this graph and the other are equal. 239 | """ 240 | return common.__eq__(self, other) and labeling.__eq__(self, other) 241 | 242 | def __ne__(self, other): 243 | """ 244 | Return whether this graph is not equal to another one. 245 | 246 | @type other: graph, digraph 247 | @param other: Other graph or digraph 248 | 249 | @rtype: boolean 250 | @return: Whether this graph and the other are different. 251 | """ 252 | return not (self == other) 253 | -------------------------------------------------------------------------------- /pygraph/classes/exceptions.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2008-2009 Pedro Matiello 2 | # Salim Fadhley 3 | # 4 | # Permission is hereby granted, free of charge, to any person 5 | # obtaining a copy of this software and associated documentation 6 | # files (the "Software"), to deal in the Software without 7 | # restriction, including without limitation the rights to use, 8 | # copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the 10 | # Software is furnished to do so, subject to the following 11 | # conditions: 12 | 13 | # The above copyright notice and this permission notice shall be 14 | # included in all copies or substantial portions of the Software. 15 | 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 18 | # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 20 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 21 | # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 23 | # OTHER DEALINGS IN THE SOFTWARE. 24 | 25 | 26 | """ 27 | Exceptions. 28 | """ 29 | 30 | # Graph errors 31 | 32 | 33 | class GraphError(RuntimeError): 34 | """ 35 | A base-class for the various kinds of errors that occur in the the python-graph class. 36 | """ 37 | 38 | pass 39 | 40 | 41 | class AdditionError(GraphError): 42 | """ 43 | This error is raised when trying to add a node or edge already added to the graph or digraph. 44 | """ 45 | 46 | pass 47 | 48 | 49 | class NodeUnreachable(GraphError): 50 | """ 51 | Goal could not be reached from start. 52 | """ 53 | 54 | def __init__(self, start, goal): 55 | msg = "Node %s could not be reached from node %s" % (repr(goal), repr(start)) 56 | InvalidGraphType.__init__(self, msg) 57 | self.start = start 58 | self.goal = goal 59 | 60 | 61 | class InvalidGraphType(GraphError): 62 | """ 63 | Invalid graph type. 64 | """ 65 | 66 | pass 67 | 68 | 69 | # Algorithm errors 70 | 71 | 72 | class AlgorithmError(RuntimeError): 73 | """ 74 | A base-class for the various kinds of errors that occur in the the 75 | algorithms package. 76 | """ 77 | 78 | pass 79 | 80 | 81 | class NegativeWeightCycleError(AlgorithmError): 82 | """ 83 | Algorithms like the Bellman-Ford algorithm can detect and raise an exception 84 | when they encounter a negative weight cycle. 85 | 86 | @see: pygraph.algorithms.shortest_path_bellman_ford 87 | """ 88 | 89 | pass 90 | -------------------------------------------------------------------------------- /pygraph/classes/graph.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2007-2009 Pedro Matiello 2 | # Johannes Reinhardt 3 | # Nathan Davis 4 | # Zsolt Haraszti 5 | # 6 | # Permission is hereby granted, free of charge, to any person 7 | # obtaining a copy of this software and associated documentation 8 | # files (the "Software"), to deal in the Software without 9 | # restriction, including without limitation the rights to use, 10 | # copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | # copies of the Software, and to permit persons to whom the 12 | # Software is furnished to do so, subject to the following 13 | # conditions: 14 | 15 | # The above copyright notice and this permission notice shall be 16 | # included in all copies or substantial portions of the Software. 17 | 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 19 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 20 | # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 21 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 22 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 23 | # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 24 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 25 | # OTHER DEALINGS IN THE SOFTWARE. 26 | 27 | 28 | """ 29 | Graph class 30 | """ 31 | 32 | # Imports 33 | from pygraph.classes.exceptions import AdditionError 34 | from pygraph.mixins.labeling import labeling 35 | from pygraph.mixins.common import common 36 | from pygraph.mixins.basegraph import basegraph 37 | 38 | 39 | class graph(basegraph, common, labeling): 40 | """ 41 | Graph class. 42 | 43 | Graphs are built of nodes and edges. 44 | 45 | @sort: __eq__, __init__, __ne__, add_edge, add_node, del_edge, del_node, edges, has_edge, has_node, 46 | neighbors, node_order, nodes 47 | """ 48 | 49 | DIRECTED = False 50 | 51 | def __init__(self): 52 | """ 53 | Initialize a graph. 54 | """ 55 | common.__init__(self) 56 | labeling.__init__(self) 57 | self.node_neighbors = {} # Pairing: Node -> Neighbors 58 | 59 | def nodes(self): 60 | """ 61 | Return node list. 62 | 63 | @rtype: list 64 | @return: Node list. 65 | """ 66 | return list(self.node_neighbors.keys()) 67 | 68 | def neighbors(self, node): 69 | """ 70 | Return all nodes that are directly accessible from given node. 71 | 72 | @type node: node 73 | @param node: Node identifier 74 | 75 | @rtype: list 76 | @return: List of nodes directly accessible from given node. 77 | """ 78 | return list(self.node_neighbors[node]) 79 | 80 | def edges(self): 81 | """ 82 | Return all edges in the graph. 83 | 84 | @rtype: list 85 | @return: List of all edges in the graph. 86 | """ 87 | return list(self.edge_properties.keys()) 88 | 89 | def has_node(self, node): 90 | """ 91 | Return whether the requested node exists. 92 | 93 | @type node: node 94 | @param node: Node identifier 95 | 96 | @rtype: boolean 97 | @return: Truth-value for node existence. 98 | """ 99 | return node in self.node_neighbors 100 | 101 | def add_node(self, node, attrs=None): 102 | """ 103 | Add given node to the graph. 104 | 105 | @attention: While nodes can be of any type, it's strongly recommended to use only 106 | numbers and single-line strings as node identifiers if you intend to use write(). 107 | 108 | @type node: node 109 | @param node: Node identifier. 110 | 111 | @type attrs: list 112 | @param attrs: List of node attributes specified as (attribute, value) tuples. 113 | """ 114 | if attrs is None: 115 | attrs = [] 116 | if node not in self.node_neighbors: 117 | self.node_neighbors[node] = set() 118 | self.node_attr[node] = attrs 119 | else: 120 | raise AdditionError("Node %s already in graph" % node) 121 | 122 | def add_edge(self, edge, wt=1, label="", attrs=[]): 123 | """ 124 | Add an edge to the graph connecting two nodes. 125 | 126 | An edge, here, is a pair of nodes like C{(n, m)}. 127 | 128 | @type edge: tuple 129 | @param edge: Edge. 130 | 131 | @type wt: number 132 | @param wt: Edge weight. 133 | 134 | @type label: string 135 | @param label: Edge label. 136 | 137 | @type attrs: list 138 | @param attrs: List of node attributes specified as (attribute, value) tuples. 139 | """ 140 | u, v = edge 141 | if v not in self.node_neighbors[u] and u not in self.node_neighbors[v]: 142 | self.node_neighbors[u].add(v) 143 | if u != v: 144 | self.node_neighbors[v].add(u) 145 | 146 | self.add_edge_attributes((u, v), attrs) 147 | self.set_edge_properties((u, v), label=label, weight=wt) 148 | else: 149 | raise AdditionError("Edge (%s, %s) already in graph" % (u, v)) 150 | 151 | def del_node(self, node): 152 | """ 153 | Remove a node from the graph. 154 | 155 | @type node: node 156 | @param node: Node identifier. 157 | """ 158 | for each in self.neighbors(node): 159 | if each != node: 160 | self.del_edge((each, node)) 161 | del self.node_neighbors[node] 162 | del self.node_attr[node] 163 | 164 | def del_edge(self, edge): 165 | """ 166 | Remove an edge from the graph. 167 | 168 | @type edge: tuple 169 | @param edge: Edge. 170 | """ 171 | u, v = edge 172 | self.node_neighbors[u].remove(v) 173 | self.del_edge_labeling((u, v)) 174 | if u != v: 175 | self.node_neighbors[v].remove(u) 176 | self.del_edge_labeling((v, u)) # TODO: This is redundant 177 | 178 | def has_edge(self, edge): 179 | """ 180 | Return whether an edge exists. 181 | 182 | @type edge: tuple 183 | @param edge: Edge. 184 | 185 | @rtype: boolean 186 | @return: Truth-value for edge existence. 187 | """ 188 | u, v = edge 189 | return (u, v) in self.edge_properties and (v, u) in self.edge_properties 190 | 191 | def node_order(self, node): 192 | """ 193 | Return the order of the graph 194 | 195 | @rtype: number 196 | @return: Order of the given node. 197 | """ 198 | return len(self.neighbors(node)) 199 | 200 | def __eq__(self, other): 201 | """ 202 | Return whether this graph is equal to another one. 203 | 204 | @type other: graph, digraph 205 | @param other: Other graph or digraph 206 | 207 | @rtype: boolean 208 | @return: Whether this graph and the other are equal. 209 | """ 210 | return common.__eq__(self, other) and labeling.__eq__(self, other) 211 | 212 | def __ne__(self, other): 213 | """ 214 | Return whether this graph is not equal to another one. 215 | 216 | @type other: graph, digraph 217 | @param other: Other graph or digraph 218 | 219 | @rtype: boolean 220 | @return: Whether this graph and the other are different. 221 | """ 222 | return not (self == other) 223 | -------------------------------------------------------------------------------- /pygraph/classes/unionfind.py: -------------------------------------------------------------------------------- 1 | class UnionFind: 2 | """ 3 | Weighted Union-Find with Path Compression 4 | """ 5 | 6 | def __init__(self, n): 7 | self._id = list(range(n)) 8 | self._sz = [1] * n 9 | 10 | def _root(self, i): 11 | j = i 12 | while j != self._id[j]: 13 | self._id[j] = self._id[self._id[j]] 14 | j = self._id[j] 15 | return j 16 | 17 | def find(self, p, q): 18 | return self._root(p) == self._root(q) 19 | 20 | def union(self, p, q): 21 | i = self._root(p) 22 | j = self._root(q) 23 | if i == j: 24 | return 25 | if self._sz[i] < self._sz[j]: 26 | self._id[i] = j 27 | self._sz[j] += self._sz[i] 28 | else: 29 | self._id[j] = i 30 | self._sz[i] += self._sz[j] 31 | -------------------------------------------------------------------------------- /pygraph/mixins/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2008-2009 Pedro Matiello 2 | # 3 | # Permission is hereby granted, free of charge, to any person 4 | # obtaining a copy of this software and associated documentation 5 | # files (the "Software"), to deal in the Software without 6 | # restriction, including without limitation the rights to use, 7 | # copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | # copies of the Software, and to permit persons to whom the 9 | # Software is furnished to do so, subject to the following 10 | # conditions: 11 | 12 | # The above copyright notice and this permission notice shall be 13 | # included in all copies or substantial portions of the Software. 14 | 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | # OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | 25 | """ 26 | Mixins. 27 | 28 | Base classes used to compose the graph classes. 29 | 30 | The classes in this namespace should not be used directly. 31 | """ 32 | -------------------------------------------------------------------------------- /pygraph/mixins/basegraph.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2008-2009 Pedro Matiello 2 | # Salim Fadhley 3 | # 4 | # Permission is hereby granted, free of charge, to any person 5 | # obtaining a copy of this software and associated documentation 6 | # files (the "Software"), to deal in the Software without 7 | # restriction, including without limitation the rights to use, 8 | # copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the 10 | # Software is furnished to do so, subject to the following 11 | # conditions: 12 | 13 | # The above copyright notice and this permission notice shall be 14 | # included in all copies or substantial portions of the Software. 15 | 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 18 | # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 20 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 21 | # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 23 | # OTHER DEALINGS IN THE SOFTWARE. 24 | 25 | 26 | class basegraph: 27 | """ 28 | An abstract class intended as a common ancestor to all graph classes. This allows the user 29 | to test isinstance(X, basegraph) to determine if the object is one of any of the python-graph 30 | main classes. 31 | """ 32 | -------------------------------------------------------------------------------- /pygraph/mixins/common.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2008-2009 Pedro Matiello 2 | # Salim Fadhley 3 | # 4 | # Permission is hereby granted, free of charge, to any person 5 | # obtaining a copy of this software and associated documentation 6 | # files (the "Software"), to deal in the Software without 7 | # restriction, including without limitation the rights to use, 8 | # copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the 10 | # Software is furnished to do so, subject to the following 11 | # conditions: 12 | 13 | # The above copyright notice and this permission notice shall be 14 | # included in all copies or substantial portions of the Software. 15 | 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 18 | # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 20 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 21 | # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 23 | # OTHER DEALINGS IN THE SOFTWARE. 24 | 25 | 26 | class common: 27 | """ 28 | Standard methods common to all graph classes. 29 | 30 | @sort: __eq__, __getitem__, __iter__, __len__, __repr__, __str__, add_graph, add_nodes, 31 | add_spanning_tree, complete, inverse, order, reverse 32 | """ 33 | 34 | def __str__(self): 35 | """ 36 | Return a string representing the graph when requested by str() (or print). 37 | 38 | @rtype: string 39 | @return: String representing the graph. 40 | """ 41 | str_nodes = repr(self.nodes()) 42 | str_edges = repr(self.edges()) 43 | return "%s %s" % (str_nodes, str_edges) 44 | 45 | def __repr__(self): 46 | """ 47 | Return a string representing the graph when requested by repr() 48 | 49 | @rtype: string 50 | @return: String representing the graph. 51 | """ 52 | return "<%s.%s %s>" % ( 53 | self.__class__.__module__, 54 | self.__class__.__name__, 55 | str(self), 56 | ) 57 | 58 | def __iter__(self): 59 | """ 60 | Return a iterator passing through all nodes in the graph. 61 | 62 | @rtype: iterator 63 | @return: Iterator passing through all nodes in the graph. 64 | """ 65 | for n in self.nodes(): 66 | yield n 67 | 68 | def __len__(self): 69 | """ 70 | Return the order of self when requested by len(). 71 | 72 | @rtype: number 73 | @return: Size of the graph. 74 | """ 75 | return self.order() 76 | 77 | def __getitem__(self, node): 78 | """ 79 | Return a iterator passing through all neighbors of the given node. 80 | 81 | @rtype: iterator 82 | @return: Iterator passing through all neighbors of the given node. 83 | """ 84 | for n in self.neighbors(node): 85 | yield n 86 | 87 | def order(self): 88 | """ 89 | Return the order of self, this is defined as the number of nodes in the graph. 90 | 91 | @rtype: number 92 | @return: Size of the graph. 93 | """ 94 | return len(self.nodes()) 95 | 96 | def add_nodes(self, nodelist): 97 | """ 98 | Add given nodes to the graph. 99 | 100 | @attention: While nodes can be of any type, it's strongly recommended to use only 101 | numbers and single-line strings as node identifiers if you intend to use write(). 102 | Objects used to identify nodes absolutely must be hashable. If you need attach a mutable 103 | or non-hashable node, consider using the labeling feature. 104 | 105 | @type nodelist: list 106 | @param nodelist: List of nodes to be added to the graph. 107 | """ 108 | for each in nodelist: 109 | self.add_node(each) 110 | 111 | def add_graph(self, other): 112 | """ 113 | Add other graph to this graph. 114 | 115 | @attention: Attributes and labels are not preserved. 116 | 117 | @type other: graph 118 | @param other: Graph 119 | """ 120 | self.add_nodes(n for n in other.nodes() if n not in self.nodes()) 121 | 122 | for each_node in other.nodes(): 123 | for each_edge in other.neighbors(each_node): 124 | if not self.has_edge((each_node, each_edge)): 125 | self.add_edge((each_node, each_edge)) 126 | 127 | def add_spanning_tree(self, st): 128 | """ 129 | Add a spanning tree to the graph. 130 | 131 | @type st: dictionary 132 | @param st: Spanning tree. 133 | """ 134 | self.add_nodes(list(st.keys())) 135 | for each in st: 136 | if st[each] is not None: 137 | self.add_edge((st[each], each)) 138 | 139 | def complete(self): 140 | """ 141 | Make the graph a complete graph. 142 | 143 | @attention: This will modify the current graph. 144 | """ 145 | for each in self.nodes(): 146 | for other in self.nodes(): 147 | if each != other and not self.has_edge((each, other)): 148 | self.add_edge((each, other)) 149 | 150 | def inverse(self): 151 | """ 152 | Return the inverse of the graph. 153 | 154 | @rtype: graph 155 | @return: Complement graph for the graph. 156 | """ 157 | inv = self.__class__() 158 | inv.add_nodes(self.nodes()) 159 | inv.complete() 160 | for each in self.edges(): 161 | if inv.has_edge(each): 162 | inv.del_edge(each) 163 | return inv 164 | 165 | def reverse(self): 166 | """ 167 | Generate the reverse of a directed graph, returns an identical graph if not directed. 168 | Attributes & weights are preserved. 169 | 170 | @rtype: digraph 171 | @return: The directed graph that should be reversed. 172 | """ 173 | assert self.DIRECTED, ( 174 | "Undirected graph types such as %s cannot be reversed" 175 | % self.__class__.__name__ 176 | ) 177 | 178 | N = self.__class__() 179 | 180 | # - Add the nodes 181 | N.add_nodes(n for n in self.nodes()) 182 | 183 | # - Add the reversed edges 184 | for u, v in self.edges(): 185 | wt = self.edge_weight((u, v)) 186 | label = self.edge_label((u, v)) 187 | attributes = self.edge_attributes((u, v)) 188 | N.add_edge((v, u), wt, label, attributes) 189 | return N 190 | 191 | def __eq__(self, other): 192 | """ 193 | Return whether this graph is equal to another one. 194 | 195 | @type other: graph, digraph 196 | @param other: Other graph or digraph 197 | 198 | @rtype: boolean 199 | @return: Whether this graph and the other are equal. 200 | """ 201 | 202 | def nodes_eq(): 203 | for each in self: 204 | if not other.has_node(each): 205 | return False 206 | for each in other: 207 | if not self.has_node(each): 208 | return False 209 | return True 210 | 211 | def edges_eq(): 212 | for edge in self.edges(): 213 | if not other.has_edge(edge): 214 | return False 215 | for edge in other.edges(): 216 | if not self.has_edge(edge): 217 | return False 218 | return True 219 | 220 | try: 221 | return nodes_eq() and edges_eq() 222 | except AttributeError: 223 | return False 224 | -------------------------------------------------------------------------------- /pygraph/mixins/labeling.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2008-2009 Pedro Matiello 2 | # Salim Fadhley 3 | # 4 | # Permission is hereby granted, free of charge, to any person 5 | # obtaining a copy of this software and associated documentation 6 | # files (the "Software"), to deal in the Software without 7 | # restriction, including without limitation the rights to use, 8 | # copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the 10 | # Software is furnished to do so, subject to the following 11 | # conditions: 12 | 13 | # The above copyright notice and this permission notice shall be 14 | # included in all copies or substantial portions of the Software. 15 | 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 18 | # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 20 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 21 | # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 23 | # OTHER DEALINGS IN THE SOFTWARE. 24 | 25 | 26 | class labeling: 27 | """ 28 | Generic labeling support for graphs 29 | 30 | @sort: __eq__, __init__, add_edge_attribute, add_edge_attributes, add_node_attribute, 31 | del_edge_labeling, del_node_labeling, edge_attributes, edge_label, edge_weight, 32 | get_edge_properties, node_attributes, set_edge_label, set_edge_properties, set_edge_weight 33 | """ 34 | 35 | WEIGHT_ATTRIBUTE_NAME = "weight" 36 | DEFAULT_WEIGHT = 1 37 | 38 | LABEL_ATTRIBUTE_NAME = "label" 39 | DEFAULT_LABEL = "" 40 | 41 | def __init__(self): 42 | # Metadata bout edges 43 | self.edge_properties = {} # Mapping: Edge -> Dict mapping, label-> str, wt->num 44 | self.edge_attr = {} # Key value pairs: (Edge -> Attributes) 45 | 46 | # Metadata bout nodes 47 | self.node_attr = {} # Pairing: Node -> Attributes 48 | 49 | def del_node_labeling(self, node): 50 | if node in self.node_attr: 51 | # Since attributes and properties are lazy, they might not exist. 52 | del self.node_attr[node] 53 | 54 | def del_edge_labeling(self, edge): 55 | keys = [edge] 56 | if not self.DIRECTED: 57 | keys.append(edge[::-1]) 58 | 59 | for key in keys: 60 | for mapping in [self.edge_properties, self.edge_attr]: 61 | try: 62 | del mapping[key] 63 | except KeyError: 64 | pass 65 | 66 | def edge_weight(self, edge): 67 | """ 68 | Get the weight of an edge. 69 | 70 | @type edge: edge 71 | @param edge: One edge. 72 | 73 | @rtype: number 74 | @return: Edge weight. 75 | """ 76 | return self.get_edge_properties(edge).setdefault( 77 | self.WEIGHT_ATTRIBUTE_NAME, self.DEFAULT_WEIGHT 78 | ) 79 | 80 | def set_edge_weight(self, edge, wt): 81 | """ 82 | Set the weight of an edge. 83 | 84 | @type edge: edge 85 | @param edge: One edge. 86 | 87 | @type wt: number 88 | @param wt: Edge weight. 89 | """ 90 | self.set_edge_properties(edge, weight=wt) 91 | if not self.DIRECTED: 92 | self.set_edge_properties((edge[1], edge[0]), weight=wt) 93 | 94 | def edge_label(self, edge): 95 | """ 96 | Get the label of an edge. 97 | 98 | @type edge: edge 99 | @param edge: One edge. 100 | 101 | @rtype: string 102 | @return: Edge label 103 | """ 104 | return self.get_edge_properties(edge).setdefault( 105 | self.LABEL_ATTRIBUTE_NAME, self.DEFAULT_LABEL 106 | ) 107 | 108 | def set_edge_label(self, edge, label): 109 | """ 110 | Set the label of an edge. 111 | 112 | @type edge: edge 113 | @param edge: One edge. 114 | 115 | @type label: string 116 | @param label: Edge label. 117 | """ 118 | self.set_edge_properties(edge, label=label) 119 | if not self.DIRECTED: 120 | self.set_edge_properties((edge[1], edge[0]), label=label) 121 | 122 | def set_edge_properties(self, edge, **properties): 123 | self.edge_properties.setdefault(edge, {}).update(properties) 124 | if not self.DIRECTED and edge[0] != edge[1]: 125 | self.edge_properties.setdefault((edge[1], edge[0]), {}).update(properties) 126 | 127 | def get_edge_properties(self, edge): 128 | return self.edge_properties.setdefault(edge, {}) 129 | 130 | def add_edge_attribute(self, edge, attr): 131 | """ 132 | Add attribute to the given edge. 133 | 134 | @type edge: edge 135 | @param edge: One edge. 136 | 137 | @type attr: tuple 138 | @param attr: Node attribute specified as a tuple in the form (attribute, value). 139 | """ 140 | self.edge_attr[edge] = self.edge_attributes(edge) + [attr] 141 | 142 | if not self.DIRECTED and edge[0] != edge[1]: 143 | self.edge_attr[(edge[1], edge[0])] = self.edge_attributes( 144 | (edge[1], edge[0]) 145 | ) + [attr] 146 | 147 | def add_edge_attributes(self, edge, attrs): 148 | """ 149 | Append a sequence of attributes to the given edge 150 | 151 | @type edge: edge 152 | @param edge: One edge. 153 | 154 | @type attrs: tuple 155 | @param attrs: Node attributes specified as a sequence of tuples in the form (attribute, value). 156 | """ 157 | for attr in attrs: 158 | self.add_edge_attribute(edge, attr) 159 | 160 | def add_node_attribute(self, node, attr): 161 | """ 162 | Add attribute to the given node. 163 | 164 | @type node: node 165 | @param node: Node identifier 166 | 167 | @type attr: tuple 168 | @param attr: Node attribute specified as a tuple in the form (attribute, value). 169 | """ 170 | self.node_attr[node] = self.node_attr[node] + [attr] 171 | 172 | def node_attributes(self, node): 173 | """ 174 | Return the attributes of the given node. 175 | 176 | @type node: node 177 | @param node: Node identifier 178 | 179 | @rtype: list 180 | @return: List of attributes specified tuples in the form (attribute, value). 181 | """ 182 | return self.node_attr[node] 183 | 184 | def edge_attributes(self, edge): 185 | """ 186 | Return the attributes of the given edge. 187 | 188 | @type edge: edge 189 | @param edge: One edge. 190 | 191 | @rtype: list 192 | @return: List of attributes specified tuples in the form (attribute, value). 193 | """ 194 | try: 195 | return self.edge_attr[edge] 196 | except KeyError: 197 | return [] 198 | 199 | def __eq__(self, other): 200 | """ 201 | Return whether this graph is equal to another one. 202 | 203 | @type other: graph, digraph 204 | @param other: Other graph or digraph 205 | 206 | @rtype: boolean 207 | @return: Whether this graph and the other are equal. 208 | """ 209 | 210 | def attrs_eq(list1, list2): 211 | for each in list1: 212 | if each not in list2: 213 | return False 214 | for each in list2: 215 | if each not in list1: 216 | return False 217 | return True 218 | 219 | def edges_eq(): 220 | for edge in self.edges(): 221 | if self.edge_weight(edge) != other.edge_weight(edge): 222 | return False 223 | if self.edge_label(edge) != other.edge_label(edge): 224 | return False 225 | if not attrs_eq( 226 | self.edge_attributes(edge), other.edge_attributes(edge) 227 | ): 228 | return False 229 | return True 230 | 231 | def nodes_eq(): 232 | for node in self: 233 | if not attrs_eq( 234 | self.node_attributes(node), other.node_attributes(node) 235 | ): 236 | return False 237 | return True 238 | 239 | return nodes_eq() and edges_eq() 240 | -------------------------------------------------------------------------------- /pygraph/readwrite/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2008-2009 Pedro Matiello 2 | # 3 | # Permission is hereby granted, free of charge, to any person 4 | # obtaining a copy of this software and associated documentation 5 | # files (the "Software"), to deal in the Software without 6 | # restriction, including without limitation the rights to use, 7 | # copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | # copies of the Software, and to permit persons to whom the 9 | # Software is furnished to do so, subject to the following 10 | # conditions: 11 | 12 | # The above copyright notice and this permission notice shall be 13 | # included in all copies or substantial portions of the Software. 14 | 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | # OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | 25 | """ 26 | Readwrite algorithms. 27 | 28 | Algorithms for reading and writing graphs. 29 | """ 30 | -------------------------------------------------------------------------------- /pygraph/readwrite/dot.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2007-2009 Pedro Matiello 2 | # 3 | # Permission is hereby granted, free of charge, to any person 4 | # obtaining a copy of this software and associated documentation 5 | # files (the "Software"), to deal in the Software without 6 | # restriction, including without limitation the rights to use, 7 | # copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | # copies of the Software, and to permit persons to whom the 9 | # Software is furnished to do so, subject to the following 10 | # conditions: 11 | 12 | # The above copyright notice and this permission notice shall be 13 | # included in all copies or substantial portions of the Software. 14 | 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | # OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | 25 | """ 26 | Functions for reading and writing graphs in Dot language. 27 | 28 | @sort: read, read_hypergraph, write, write_hypergraph 29 | """ 30 | 31 | # Imports 32 | from pygraph.classes.digraph import digraph 33 | from pygraph.classes.exceptions import InvalidGraphType 34 | from pygraph.classes.graph import graph 35 | from pygraph.classes.hypergraph import hypergraph 36 | import pydot 37 | 38 | # Values 39 | colors = [ 40 | "aquamarine4", 41 | "blue4", 42 | "brown4", 43 | "cornflowerblue", 44 | "cyan4", 45 | "darkgreen", 46 | "darkorange3", 47 | "darkorchid4", 48 | "darkseagreen4", 49 | "darkslategray", 50 | "deeppink4", 51 | "deepskyblue4", 52 | "firebrick3", 53 | "hotpink3", 54 | "indianred3", 55 | "indigo", 56 | "lightblue4", 57 | "lightseagreen", 58 | "lightskyblue4", 59 | "magenta4", 60 | "maroon", 61 | "palevioletred3", 62 | "steelblue", 63 | "violetred3", 64 | ] 65 | 66 | 67 | def read(string): 68 | """ 69 | Read a graph from a string in Dot language and return it. Nodes and edges specified in the 70 | input will be added to the current graph. 71 | 72 | @type string: string 73 | @param string: Input string in Dot format specifying a graph. 74 | 75 | @rtype: graph 76 | @return: Graph 77 | """ 78 | 79 | dotG = pydot.graph_from_dot_data(string)[0] 80 | 81 | if dotG.get_type() == "graph": 82 | G = graph() 83 | elif dotG.get_type() == "digraph": 84 | G = digraph() 85 | elif dotG.get_type() == "hypergraph": 86 | return read_hypergraph(string) 87 | else: 88 | raise InvalidGraphType 89 | 90 | # Read nodes... 91 | # Note: If the nodes aren't explicitly listed, they need to be 92 | for each_node in dotG.get_nodes(): 93 | G.add_node(each_node.get_name()) 94 | for each_attr_key, each_attr_val in each_node.get_attributes().items(): 95 | G.add_node_attribute(each_node.get_name(), (each_attr_key, each_attr_val)) 96 | 97 | # Read edges... 98 | for each_edge in dotG.get_edges(): 99 | # Check if the nodes have been added 100 | if not G.has_node(each_edge.get_source()): 101 | G.add_node(each_edge.get_source()) 102 | if not G.has_node(each_edge.get_destination()): 103 | G.add_node(each_edge.get_destination()) 104 | 105 | # See if there's a weight 106 | if "weight" in list(each_edge.get_attributes().keys()): 107 | _wt = each_edge.get_attributes()["weight"] 108 | else: 109 | _wt = 1 110 | 111 | # See if there is a label 112 | if "label" in list(each_edge.get_attributes().keys()): 113 | _label = each_edge.get_attributes()["label"] 114 | else: 115 | _label = "" 116 | 117 | G.add_edge( 118 | (each_edge.get_source(), each_edge.get_destination()), wt=_wt, label=_label 119 | ) 120 | 121 | for each_attr_key, each_attr_val in each_edge.get_attributes().items(): 122 | if each_attr_key not in ["weight", "label"]: 123 | G.add_edge_attribute( 124 | (each_edge.get_source(), each_edge.get_destination()), 125 | (each_attr_key, each_attr_val), 126 | ) 127 | 128 | return G 129 | 130 | 131 | def write(G, weighted=False): 132 | """ 133 | Return a string specifying the given graph in Dot language. 134 | 135 | @type G: graph 136 | @param G: Graph. 137 | 138 | @type weighted: boolean 139 | @param weighted: Whether edges should be labelled with their weight. 140 | 141 | @rtype: string 142 | @return: String specifying the graph in Dot Language. 143 | """ 144 | dotG = pydot.Dot() 145 | 146 | if "name" not in dir(G): 147 | dotG.set_name("graphname") 148 | else: 149 | dotG.set_name(G.name) 150 | 151 | if isinstance(G, graph): 152 | dotG.set_type("graph") 153 | directed = False 154 | elif isinstance(G, digraph): 155 | dotG.set_type("digraph") 156 | directed = True 157 | elif isinstance(G, hypergraph): 158 | return write_hypergraph(G) 159 | else: 160 | raise InvalidGraphType("Expected graph or digraph, got %s" % repr(G)) 161 | 162 | for node in G.nodes(): 163 | attr_list = {} 164 | for attr in G.node_attributes(node): 165 | attr_list[str(attr[0])] = str(attr[1]) 166 | 167 | newNode = pydot.Node(str(node), **attr_list) 168 | 169 | dotG.add_node(newNode) 170 | 171 | # Pydot doesn't work properly with the get_edge, so we use 172 | # our own set to keep track of what's been added or not. 173 | seen_edges = set([]) 174 | for edge_from, edge_to in G.edges(): 175 | if (str(edge_from) + "-" + str(edge_to)) in seen_edges: 176 | continue 177 | 178 | if (not directed) and (str(edge_to) + "-" + str(edge_from)) in seen_edges: 179 | continue 180 | 181 | attr_list = {} 182 | for attr in G.edge_attributes((edge_from, edge_to)): 183 | attr_list[str(attr[0])] = str(attr[1]) 184 | 185 | if str(G.edge_label((edge_from, edge_to))): 186 | attr_list["label"] = str(G.edge_label((edge_from, edge_to))) 187 | 188 | elif weighted: 189 | attr_list["label"] = str(G.edge_weight((edge_from, edge_to))) 190 | 191 | if weighted: 192 | attr_list["weight"] = str(G.edge_weight((edge_from, edge_to))) 193 | 194 | newEdge = pydot.Edge(str(edge_from), str(edge_to), **attr_list) 195 | 196 | dotG.add_edge(newEdge) 197 | 198 | seen_edges.add(str(edge_from) + "-" + str(edge_to)) 199 | 200 | return dotG.to_string() 201 | 202 | 203 | def read_hypergraph(string): 204 | """ 205 | Read a hypergraph from a string in dot format. Nodes and edges specified in the input will be 206 | added to the current hypergraph. 207 | 208 | @type string: string 209 | @param string: Input string in dot format specifying a graph. 210 | 211 | @rtype: hypergraph 212 | @return: Hypergraph 213 | """ 214 | hgr = hypergraph() 215 | dotG = pydot.graph_from_dot_data(string)[0] 216 | 217 | # Read the hypernode nodes... 218 | # Note 1: We need to assume that all of the nodes are listed since we need to know if they 219 | # are a hyperedge or a normal node 220 | # Note 2: We should read in all of the nodes before putting in the links 221 | for each_node in dotG.get_nodes(): 222 | if "hypernode" == each_node.get("hyper_node_type"): 223 | hgr.add_node(each_node.get_name()) 224 | elif "hyperedge" == each_node.get("hyper_node_type"): 225 | hgr.add_hyperedge(each_node.get_name()) 226 | 227 | # Now read in the links to connect the hyperedges 228 | for each_link in dotG.get_edges(): 229 | if hgr.has_node(each_link.get_source()): 230 | link_hypernode = each_link.get_source() 231 | link_hyperedge = each_link.get_destination() 232 | elif hgr.has_node(each_link.get_destination()): 233 | link_hypernode = each_link.get_destination() 234 | link_hyperedge = each_link.get_source() 235 | hgr.link(link_hypernode, link_hyperedge) 236 | 237 | return hgr 238 | 239 | 240 | def write_hypergraph(hgr, colored=False): 241 | """ 242 | Return a string specifying the given hypergraph in DOT Language. 243 | 244 | @type hgr: hypergraph 245 | @param hgr: Hypergraph. 246 | 247 | @type colored: boolean 248 | @param colored: Whether hyperedges should be colored. 249 | 250 | @rtype: string 251 | @return: String specifying the hypergraph in DOT Language. 252 | """ 253 | dotG = pydot.Dot() 254 | 255 | if "name" not in dir(hgr): 256 | dotG.set_name("hypergraph") 257 | else: 258 | dotG.set_name(hgr.name) 259 | 260 | colortable = {} 261 | colorcount = 0 262 | 263 | # Add all of the nodes first 264 | for node in hgr.nodes(): 265 | newNode = pydot.Node(str(node), hyper_node_type="hypernode") 266 | 267 | dotG.add_node(newNode) 268 | 269 | for hyperedge in hgr.hyperedges(): 270 | if colored: 271 | colortable[hyperedge] = colors[colorcount % len(colors)] 272 | colorcount += 1 273 | 274 | newNode = pydot.Node( 275 | str(hyperedge), 276 | hyper_node_type="hyperedge", 277 | color=str(colortable[hyperedge]), 278 | shape="point", 279 | ) 280 | else: 281 | newNode = pydot.Node(str(hyperedge), hyper_node_type="hyperedge") 282 | 283 | dotG.add_node(newNode) 284 | 285 | for link in hgr.links(hyperedge): 286 | newEdge = pydot.Edge(str(hyperedge), str(link)) 287 | dotG.add_edge(newEdge) 288 | 289 | return dotG.to_string() 290 | -------------------------------------------------------------------------------- /pygraph/readwrite/markup.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2007-2009 Pedro Matiello 2 | # 3 | # Permission is hereby granted, free of charge, to any person 4 | # obtaining a copy of this software and associated documentation 5 | # files (the "Software"), to deal in the Software without 6 | # restriction, including without limitation the rights to use, 7 | # copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | # copies of the Software, and to permit persons to whom the 9 | # Software is furnished to do so, subject to the following 10 | # conditions: 11 | 12 | # The above copyright notice and this permission notice shall be 13 | # included in all copies or substantial portions of the Software. 14 | 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | # OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | 25 | """ 26 | Functions for reading and writing graphs in a XML markup. 27 | 28 | @sort: read, read_hypergraph, write, write_hypergraph 29 | """ 30 | 31 | # Imports 32 | from pygraph.classes.digraph import digraph 33 | from pygraph.classes.exceptions import InvalidGraphType 34 | from pygraph.classes.graph import graph 35 | from pygraph.classes.hypergraph import hypergraph 36 | from xml.dom.minidom import Document, parseString 37 | 38 | 39 | def write(G): 40 | """ 41 | Return a string specifying the given graph as a XML document. 42 | 43 | @type G: graph 44 | @param G: Graph. 45 | 46 | @rtype: string 47 | @return: String specifying the graph as a XML document. 48 | """ 49 | 50 | # Document root 51 | grxml = Document() 52 | if type(G) == graph: 53 | grxmlr = grxml.createElement("graph") 54 | elif type(G) == digraph: 55 | grxmlr = grxml.createElement("digraph") 56 | elif type(G) == hypergraph: 57 | return write_hypergraph(G) 58 | else: 59 | raise InvalidGraphType 60 | grxml.appendChild(grxmlr) 61 | 62 | # Each node... 63 | for each_node in G.nodes(): 64 | node = grxml.createElement("node") 65 | node.setAttribute("id", str(each_node)) 66 | grxmlr.appendChild(node) 67 | for each_attr in G.node_attributes(each_node): 68 | attr = grxml.createElement("attribute") 69 | attr.setAttribute("attr", each_attr[0]) 70 | attr.setAttribute("value", each_attr[1]) 71 | node.appendChild(attr) 72 | 73 | # Each edge... 74 | for edge_from, edge_to in G.edges(): 75 | edge = grxml.createElement("edge") 76 | edge.setAttribute("from", str(edge_from)) 77 | edge.setAttribute("to", str(edge_to)) 78 | edge.setAttribute("wt", str(G.edge_weight((edge_from, edge_to)))) 79 | edge.setAttribute("label", str(G.edge_label((edge_from, edge_to)))) 80 | grxmlr.appendChild(edge) 81 | for attr_name, attr_value in G.edge_attributes((edge_from, edge_to)): 82 | attr = grxml.createElement("attribute") 83 | attr.setAttribute("attr", attr_name) 84 | attr.setAttribute("value", attr_value) 85 | edge.appendChild(attr) 86 | 87 | return grxml.toprettyxml() 88 | 89 | 90 | def read(string): 91 | """ 92 | Read a graph from a XML document and return it. Nodes and edges specified in the input will 93 | be added to the current graph. 94 | 95 | @type string: string 96 | @param string: Input string in XML format specifying a graph. 97 | 98 | @rtype: graph 99 | @return: Graph 100 | """ 101 | dom = parseString(string) 102 | if dom.getElementsByTagName("graph"): 103 | G = graph() 104 | elif dom.getElementsByTagName("digraph"): 105 | G = digraph() 106 | elif dom.getElementsByTagName("hypergraph"): 107 | return read_hypergraph(string) 108 | else: 109 | raise InvalidGraphType 110 | 111 | # Read nodes... 112 | for each_node in dom.getElementsByTagName("node"): 113 | G.add_node(each_node.getAttribute("id")) 114 | for each_attr in each_node.getElementsByTagName("attribute"): 115 | G.add_node_attribute( 116 | each_node.getAttribute("id"), 117 | (each_attr.getAttribute("attr"), each_attr.getAttribute("value")), 118 | ) 119 | 120 | # Read edges... 121 | for each_edge in dom.getElementsByTagName("edge"): 122 | if not G.has_edge( 123 | (each_edge.getAttribute("from"), each_edge.getAttribute("to")) 124 | ): 125 | G.add_edge( 126 | (each_edge.getAttribute("from"), each_edge.getAttribute("to")), 127 | wt=float(each_edge.getAttribute("wt")), 128 | label=each_edge.getAttribute("label"), 129 | ) 130 | for each_attr in each_edge.getElementsByTagName("attribute"): 131 | attr_tuple = ( 132 | each_attr.getAttribute("attr"), 133 | each_attr.getAttribute("value"), 134 | ) 135 | if attr_tuple not in G.edge_attributes( 136 | (each_edge.getAttribute("from"), each_edge.getAttribute("to")) 137 | ): 138 | G.add_edge_attribute( 139 | (each_edge.getAttribute("from"), each_edge.getAttribute("to")), 140 | attr_tuple, 141 | ) 142 | 143 | return G 144 | 145 | 146 | def write_hypergraph(hgr): 147 | """ 148 | Return a string specifying the given hypergraph as a XML document. 149 | 150 | @type hgr: hypergraph 151 | @param hgr: Hypergraph. 152 | 153 | @rtype: string 154 | @return: String specifying the graph as a XML document. 155 | """ 156 | 157 | # Document root 158 | grxml = Document() 159 | grxmlr = grxml.createElement("hypergraph") 160 | grxml.appendChild(grxmlr) 161 | 162 | # Each node... 163 | nodes = hgr.nodes() 164 | hyperedges = hgr.hyperedges() 165 | for each_node in nodes + hyperedges: 166 | if each_node in nodes: 167 | node = grxml.createElement("node") 168 | else: 169 | node = grxml.createElement("hyperedge") 170 | node.setAttribute("id", str(each_node)) 171 | grxmlr.appendChild(node) 172 | 173 | # and its outgoing edge 174 | if each_node in nodes: 175 | for each_edge in hgr.links(each_node): 176 | edge = grxml.createElement("link") 177 | edge.setAttribute("to", str(each_edge)) 178 | node.appendChild(edge) 179 | 180 | return grxml.toprettyxml() 181 | 182 | 183 | def read_hypergraph(string): 184 | """ 185 | Read a graph from a XML document. Nodes and hyperedges specified in the input will be added 186 | to the current graph. 187 | 188 | @type string: string 189 | @param string: Input string in XML format specifying a graph. 190 | 191 | @rtype: hypergraph 192 | @return: Hypergraph 193 | """ 194 | 195 | hgr = hypergraph() 196 | 197 | dom = parseString(string) 198 | for each_node in dom.getElementsByTagName("node"): 199 | hgr.add_node(each_node.getAttribute("id")) 200 | for each_node in dom.getElementsByTagName("hyperedge"): 201 | hgr.add_hyperedge(each_node.getAttribute("id")) 202 | dom = parseString(string) 203 | for each_node in dom.getElementsByTagName("node"): 204 | for each_edge in each_node.getElementsByTagName("link"): 205 | hgr.link( 206 | str(each_node.getAttribute("id")), str(each_edge.getAttribute("to")) 207 | ) 208 | return hgr 209 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "python-graph" 3 | version = "2.0.1.dev0" 4 | description = "A library for working with graphs in Python" 5 | authors = [ 6 | {name = "Pedro Matiello"} 7 | ] 8 | license = {text = "MIT"} 9 | readme = "README.rst" 10 | requires-python = ">=3.10" 11 | dependencies = [] 12 | 13 | [project.optional-dependencies] 14 | dot = [ 15 | "pydot" 16 | ] 17 | 18 | [tool.poetry] 19 | packages = [ 20 | { include = "pygraph" }, 21 | ] 22 | 23 | [tool.poetry.group.dev.dependencies] 24 | pytest = "^8.3.5" 25 | coverage = "^7.8.2" 26 | ruff = "^0.11.11" 27 | pydot = "^4.0.0" 28 | 29 | [build-system] 30 | requires = ["poetry-core>=2.0.0,<3.0.0"] 31 | build-backend = "poetry.core.masonry.api" 32 | 33 | [tool.pytest.ini_options] 34 | #minversion = "6.0" 35 | addopts = "--doctest-modules --maxfail=1" 36 | doctest_optionflags = [ 37 | "NORMALIZE_WHITESPACE", 38 | "IGNORE_EXCEPTION_DETAIL", 39 | "ELLIPSIS", 40 | ] 41 | testpaths = [ 42 | "pygraph", 43 | "tests", 44 | ] 45 | python_files = [ 46 | "test_*", 47 | "unittests-*" 48 | ] 49 | 50 | [tool.coverage.run] 51 | command_line = "-m pytest --doctest-modules" 52 | source = ["pygraph"] 53 | 54 | [tool.coverage.report] 55 | show_missing = true 56 | skip_covered = true 57 | skip_empty = true 58 | exclude_also = [ 59 | 'def __repr__', 60 | 'if self.debug:', 61 | 'raise AssertionError', 62 | 'raise NotImplementedError', 63 | 'if 0:', 64 | 'if __name__ == .__main__.:', 65 | 'if TYPE_CHECKING:', 66 | 'class .*\bProtocol\):', 67 | '@(abc\.)?abstractmethod', 68 | ] 69 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Shoobx/python-graph/0ec006fa7fa58f1e40dfa94b1bdfd9ae78837da7/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_data.py: -------------------------------------------------------------------------------- 1 | """ 2 | Misc functions used for testing, including the generation of test-data. 3 | """ 4 | 5 | EDGES = [ 6 | ("China", "Russia"), 7 | ("Afghanistan", "Iran"), 8 | ("China", "Russia"), 9 | ("China", "Mongolia"), 10 | ("Mongolia", "Russia"), 11 | ("Mongolia", "China"), 12 | ("Nepal", "China"), 13 | ("India", "Pakistan"), 14 | ("India", "Nepal"), 15 | ("Afghanistan", "Pakistan"), 16 | ("North Korea", "China"), 17 | ("Romania", "Bulgaria"), 18 | ("Romania", "Moldova"), 19 | ("Romania", "Ukraine"), 20 | ("Romania", "Hungary"), 21 | ("North Korea", "South Korea"), 22 | ("Portugal", "Spain"), 23 | ("Spain","France"), 24 | ("France","Belgium"), 25 | ("France","Germany"), 26 | ("France","Italy",), 27 | ("Belgium","Netherlands"), 28 | ("Germany","Belgium"), 29 | ("Germany","Netherlands"), 30 | ("Germany","Denmark"), 31 | ("Germany","Luxembourg"), 32 | ("Germany","Czech Republic"), 33 | ("Belgium","Luxembourg"), 34 | ("France","Luxembourg"), 35 | ("England","Wales"), 36 | ("England","Scotland"), 37 | ("England","France"), 38 | ("Scotland","Wales"), 39 | ("Scotland","Ireland"), 40 | ("England","Ireland"), 41 | ("Switzerland","Austria"), 42 | ("Switzerland","Germany"), 43 | ("Switzerland","France"), 44 | ("Switzerland","Italy"), 45 | ("Austria","Germany"), 46 | ("Austria","Italy"), 47 | ("Austria","Czech Republic"), 48 | ("Austria","Slovakia"), 49 | ("Austria","Hungary"), 50 | ("Austria","Slovenia"), 51 | ("Denmark","Germany"), 52 | ("Poland","Czech Republic"), 53 | ("Poland","Slovakia"), 54 | ("Poland","Germany"), 55 | ("Poland","Russia"), 56 | ("Poland","Ukraine"), 57 | ("Poland","Belarus"), 58 | ("Poland","Lithuania"), 59 | ("Czech Republic","Slovakia"), 60 | ("Czech Republic","Germany"), 61 | ("Slovakia","Hungary")] 62 | 63 | 64 | def nations_of_the_world( G ): 65 | """ 66 | This is intended to simplify the unit-tests. Given a graph add the nations of the world to it. 67 | """ 68 | for a,b in EDGES: 69 | 70 | for n in [a,b,]: 71 | if not n in G.nodes(): 72 | G.add_node(n) 73 | 74 | if (not G.has_edge((a,b))): 75 | G.add_edge( (a,b) ) 76 | 77 | return G 78 | 79 | 80 | -------------------------------------------------------------------------------- /tests/testlib.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2007-2009 Pedro Matiello 2 | # 3 | # Permission is hereby granted, free of charge, to any person 4 | # obtaining a copy of this software and associated documentation 5 | # files (the "Software"), to deal in the Software without 6 | # restriction, including without limitation the rights to use, 7 | # copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | # copies of the Software, and to permit persons to whom the 9 | # Software is furnished to do so, subject to the following 10 | # conditions: 11 | 12 | # The above copyright notice and this permission notice shall be 13 | # included in all copies or substantial portions of the Software. 14 | 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | # OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | 25 | """ 26 | Helper functions for unit-tests. 27 | """ 28 | 29 | 30 | # Imports 31 | from pygraph.algorithms.generators import generate, generate_hypergraph 32 | from random import seed 33 | from time import time 34 | from sys import argv 35 | 36 | # Configuration 37 | random_seed = int(time()) 38 | num_nodes = { 'small': 10, 39 | 'medium': 25, 40 | 'sparse': 40 41 | } 42 | num_edges = { 'small': 18, 43 | 'medium': 120, 44 | 'sparse': 200 45 | } 46 | sizes = ['small', 'medium', 'sparse'] 47 | use_size = 'medium' 48 | 49 | # Init 50 | try: 51 | if (argv[0] != 'testrunner.py'): 52 | print() 53 | print(("Random seed: %s" % random_seed)) 54 | except: 55 | pass 56 | 57 | 58 | def new_graph(wt_range=(1, 1)): 59 | seed(random_seed) 60 | return generate(num_nodes[use_size], num_edges[use_size], directed=False, weight_range=wt_range) 61 | 62 | def new_digraph(wt_range=(1, 1)): 63 | seed(random_seed) 64 | return generate(num_nodes[use_size], num_edges[use_size], directed=True, weight_range=wt_range) 65 | 66 | def new_hypergraph(): 67 | seed(random_seed) 68 | return generate_hypergraph(num_nodes[use_size], num_edges[use_size]) 69 | 70 | def new_uniform_hypergraph(_r): 71 | seed(random_seed) 72 | return generate_hypergraph(num_nodes[use_size], num_edges[use_size], r = _r) 73 | -------------------------------------------------------------------------------- /tests/unittests-critical.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Pedro Matiello 2 | # Tomaz Kovacic 3 | # 4 | # Permission is hereby granted, free of charge, to any person 5 | # obtaining a copy of this software and associated documentation 6 | # files (the "Software"), to deal in the Software without 7 | # restriction, including without limitation the rights to use, 8 | # copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the 10 | # Software is furnished to do so, subject to the following 11 | # conditions: 12 | 13 | # The above copyright notice and this permission notice shall be 14 | # included in all copies or substantial portions of the Software. 15 | 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 18 | # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 20 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 21 | # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 23 | # OTHER DEALINGS IN THE SOFTWARE. 24 | 25 | 26 | """ 27 | Unittests for pygraph.algorithms.critical 28 | """ 29 | 30 | import unittest 31 | from pygraph.algorithms.critical import critical_path 32 | from pygraph.algorithms.critical import transitive_edges,_intersection 33 | from pygraph.classes.digraph import digraph 34 | 35 | def generate_test_graph(): 36 | ''' 37 | Generates & returns a weighted digraph with 38 | one transitive edge and no cycles. 39 | ''' 40 | G = digraph() 41 | G.add_nodes([1,2,3,4,5,6]) 42 | G.add_edge((1,2), 1) 43 | G.add_edge((2,4), 4) 44 | G.add_edge((1,3), 1)#transitive edge 45 | G.add_edge((2,3), 20) 46 | G.add_edge((3,5), 3) 47 | G.add_edge((4,6), 5) 48 | G.add_edge((5,6), 4) 49 | return G 50 | 51 | class test_critical_path_and_transitive_edges(unittest.TestCase): 52 | 53 | # critical path algorithm 54 | 55 | def test_critical_path_with_cycle(self): 56 | G = generate_test_graph() 57 | G.add_edge((5,2),3)#add cycle 58 | assert critical_path(G) == [] 59 | 60 | def test_critical_path(self): 61 | G = generate_test_graph() 62 | assert critical_path(G) == [1,2,3,5,6] 63 | 64 | # transitive edge detection algorithm 65 | 66 | def test_transitivity_with_cycle(self): 67 | G = generate_test_graph() 68 | G.add_edge((5,2),3)#add cycle 69 | assert transitive_edges(G) == [] 70 | 71 | def test_transitivity(self): 72 | G = generate_test_graph() 73 | G.add_edge((2,5),1)#add another transitive edge 74 | assert transitive_edges(G) == [(1,3),(2,5)] 75 | 76 | # intersection testing (used internally) 77 | 78 | def test_partial_intersection(self): 79 | list1 = [1,2,3,4] 80 | list2 = [3,4,5,6] 81 | assert _intersection(list1, list2) == [3,4] 82 | 83 | def test_empty_intersection(self): 84 | list1 = [1,2,3,4] 85 | list2 = [5,6] 86 | assert _intersection(list1, list2) == [] 87 | 88 | 89 | if __name__ == "__main__": 90 | unittest.main() -------------------------------------------------------------------------------- /tests/unittests-cycles.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Pedro Matiello 2 | # 3 | # Permission is hereby granted, free of charge, to any person 4 | # obtaining a copy of this software and associated documentation 5 | # files (the "Software"), to deal in the Software without 6 | # restriction, including without limitation the rights to use, 7 | # copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | # copies of the Software, and to permit persons to whom the 9 | # Software is furnished to do so, subject to the following 10 | # conditions: 11 | 12 | # The above copyright notice and this permission notice shall be 13 | # included in all copies or substantial portions of the Software. 14 | 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | # OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | 25 | """ 26 | Unittests for graph.algorithms.cycles 27 | """ 28 | 29 | 30 | import unittest 31 | import pygraph 32 | from pygraph.algorithms.cycles import find_cycle 33 | from pygraph.algorithms.searching import depth_first_search 34 | from pygraph.classes.digraph import digraph 35 | from pygraph.classes.graph import graph 36 | from sys import getrecursionlimit 37 | from . import testlib 38 | 39 | 40 | def verify_cycle(graph, cycle): 41 | for i in range(len(cycle)): 42 | assert graph.has_edge((cycle[i],cycle[(i+1)%len(cycle)])) 43 | 44 | class test_find_cycle(unittest.TestCase): 45 | 46 | # Graph 47 | 48 | def test_find_cycle_on_graph(self): 49 | gr = testlib.new_graph() 50 | cycle = find_cycle(gr) 51 | verify_cycle(gr, cycle) 52 | 53 | def test_find_cycle_on_graph_withot_cycles(self): 54 | gr = testlib.new_graph() 55 | st, pre, post = depth_first_search(gr) 56 | gr = graph() 57 | gr.add_spanning_tree(st) 58 | assert find_cycle(gr) == [] 59 | 60 | # Digraph 61 | 62 | def test_find_cycle_on_digraph(self): 63 | gr = testlib.new_digraph() 64 | cycle = find_cycle(gr) 65 | verify_cycle(gr, cycle) 66 | 67 | def test_find_cycle_on_digraph_without_cycles(self): 68 | gr = testlib.new_digraph() 69 | st, pre, post = depth_first_search(gr) 70 | gr = digraph() 71 | gr.add_spanning_tree(st) 72 | assert find_cycle(gr) == [] 73 | 74 | def test_find_small_cycle_on_digraph(self): 75 | gr = digraph() 76 | gr.add_nodes([1, 2, 3, 4, 5]) 77 | gr.add_edge((1, 2)) 78 | gr.add_edge((2, 3)) 79 | gr.add_edge((2, 4)) 80 | gr.add_edge((4, 5)) 81 | gr.add_edge((2, 1)) 82 | # Cycle: 1-2 83 | assert find_cycle(gr) == [1,2] 84 | 85 | def test_find_cycle_on_very_deep_graph(self): 86 | gr = pygraph.classes.graph.graph() 87 | gr.add_nodes(list(range(0,12001))) 88 | for i in range(0,12000): 89 | gr.add_edge((i,i+1)) 90 | recursionlimit = getrecursionlimit() 91 | find_cycle(gr) 92 | assert getrecursionlimit() == recursionlimit 93 | 94 | # Regression 95 | 96 | def test_regression1(self): 97 | G = digraph() 98 | G.add_nodes([1, 2, 3, 4, 5]) 99 | G.add_edge((1, 2)) 100 | G.add_edge((2, 3)) 101 | G.add_edge((2, 4)) 102 | G.add_edge((4, 5)) 103 | G.add_edge((3, 5)) 104 | G.add_edge((3, 1)) 105 | assert find_cycle(G) == [1, 2, 3] 106 | 107 | if __name__ == "__main__": 108 | unittest.main() 109 | -------------------------------------------------------------------------------- /tests/unittests-digraph.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Pedro Matiello 2 | # 3 | # Permission is hereby granted, free of charge, to any person 4 | # obtaining a copy of this software and associated documentation 5 | # files (the "Software"), to deal in the Software without 6 | # restriction, including without limitation the rights to use, 7 | # copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | # copies of the Software, and to permit persons to whom the 9 | # Software is furnished to do so, subject to the following 10 | # conditions: 11 | 12 | # The above copyright notice and this permission notice shall be 13 | # included in all copies or substantial portions of the Software. 14 | 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | # OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | 25 | """ 26 | Unittests for graph.classes.Digraph 27 | """ 28 | 29 | import unittest 30 | from pygraph.classes.exceptions import AdditionError 31 | from pygraph.classes.digraph import digraph 32 | from pygraph.classes.graph import graph 33 | from . import testlib 34 | from copy import copy, deepcopy 35 | 36 | 37 | class test_digraph(unittest.TestCase): 38 | # Add/Remove nodes and edges 39 | 40 | def test_raise_exception_on_duplicate_node_addition(self): 41 | gr = digraph() 42 | gr.add_node("a_node") 43 | try: 44 | gr.add_node("a_node") 45 | except AdditionError: 46 | pass 47 | else: 48 | fail() 49 | 50 | def test_raise_exception_on_duplicate_edge_addition(self): 51 | gr = digraph() 52 | gr.add_node("a_node") 53 | gr.add_node("other_node") 54 | gr.add_edge(("a_node", "other_node")) 55 | try: 56 | gr.add_edge(("a_node", "other_node")) 57 | except AdditionError: 58 | pass 59 | else: 60 | fail() 61 | 62 | def test_raise_exception_when_edge_added_from_non_existing_node(self): 63 | gr = digraph() 64 | gr.add_nodes([0, 1]) 65 | try: 66 | gr.add_edge((3, 0)) 67 | except AdditionError: 68 | pass 69 | else: 70 | self.fail("The graph allowed an edge to be added from a non-existing node.") 71 | assert gr.node_neighbors == {0: [], 1: []} 72 | assert gr.node_incidence == {0: [], 1: []} 73 | 74 | def test_raise_exception_when_edge_added_to_non_existing_node(self): 75 | gr = digraph() 76 | gr.add_nodes([0, 1]) 77 | try: 78 | gr.add_edge((0, 3)) 79 | except AdditionError: 80 | pass 81 | else: 82 | self.fail("TThe graph allowed an edge to be added to a non-existing node.") 83 | assert gr.node_neighbors == {0: [], 1: []} 84 | assert gr.node_incidence == {0: [], 1: []} 85 | 86 | def test_remove_node(self): 87 | gr = testlib.new_digraph() 88 | gr.del_node(0) 89 | self.assertTrue(0 not in gr) 90 | for each, other in gr.edges(): 91 | self.assertTrue(each in gr) 92 | self.assertTrue(other in gr) 93 | 94 | def test_remove_edge_from_node_to_same_node(self): 95 | gr = digraph() 96 | gr.add_node(0) 97 | gr.add_edge((0, 0)) 98 | gr.del_edge((0, 0)) 99 | 100 | def test_remove_node_with_edge_to_itself(self): 101 | gr = digraph() 102 | gr.add_node(0) 103 | gr.add_edge((0, 0)) 104 | gr.del_node(0) 105 | 106 | # Invert graph 107 | 108 | def test_invert_digraph(self): 109 | gr = testlib.new_digraph() 110 | inv = gr.inverse() 111 | for each in gr.edges(): 112 | self.assertTrue(each not in inv.edges()) 113 | for each in inv.edges(): 114 | self.assertTrue(each not in gr.edges()) 115 | 116 | def test_invert_empty_digraph(self): 117 | gr = digraph() 118 | inv = gr.inverse() 119 | self.assertTrue(gr.nodes() == []) 120 | self.assertTrue(gr.edges() == []) 121 | 122 | # Reverse graph 123 | def test_reverse_digraph(self): 124 | gr = testlib.new_digraph() 125 | rev = gr.reverse() 126 | for u, v in gr.edges(): 127 | self.assertTrue((v, u) in rev.edges()) 128 | for u, v in rev.edges(): 129 | self.assertTrue((v, u) in gr.edges()) 130 | 131 | def test_invert_empty_digraph(self): 132 | gr = digraph() 133 | rev = gr.reverse() 134 | self.assertTrue(rev.nodes() == []) 135 | self.assertTrue(rev.edges() == []) 136 | 137 | # Complete graph 138 | 139 | def test_complete_digraph(self): 140 | gr = digraph() 141 | gr.add_nodes(list(range(10))) 142 | gr.complete() 143 | for i in range(10): 144 | for j in range(10): 145 | self.assertTrue((i, j) in gr.edges() or i == j) 146 | 147 | def test_complete_empty_digraph(self): 148 | gr = digraph() 149 | gr.complete() 150 | self.assertTrue(gr.nodes() == []) 151 | self.assertTrue(gr.edges() == []) 152 | 153 | def test_complete_digraph_with_one_node(self): 154 | gr = digraph() 155 | gr.add_node(0) 156 | gr.complete() 157 | self.assertTrue(gr.nodes() == [0]) 158 | self.assertTrue(gr.edges() == []) 159 | 160 | # Add graph 161 | 162 | def test_add_digraph(self): 163 | gr1 = testlib.new_digraph() 164 | gr2 = testlib.new_digraph() 165 | gr1.add_graph(gr2) 166 | for each in gr2.nodes(): 167 | self.assertTrue(each in gr1) 168 | for each in gr2.edges(): 169 | self.assertTrue(each in gr1.edges()) 170 | 171 | def test_add_empty_digraph(self): 172 | gr1 = testlib.new_digraph() 173 | gr1c = copy(gr1) 174 | gr2 = digraph() 175 | gr1.add_graph(gr2) 176 | self.assertTrue(gr1.nodes() == gr1c.nodes()) 177 | self.assertTrue(gr1.edges() == gr1c.edges()) 178 | 179 | def test_add_graph_into_diagraph(self): 180 | d = digraph() 181 | g = graph() 182 | 183 | A = "A" 184 | B = "B" 185 | 186 | g.add_node(A) 187 | g.add_node(B) 188 | g.add_edge((A, B)) 189 | 190 | d.add_graph(g) 191 | 192 | assert d.has_node(A) 193 | assert d.has_node(B) 194 | assert d.has_edge((A, B)) 195 | assert d.has_edge((B, A)) 196 | 197 | # Add spanning tree 198 | 199 | def test_add_spanning_tree(self): 200 | gr = digraph() 201 | st = {0: None, 1: 0, 2: 0, 3: 1, 4: 2, 5: 3} 202 | gr.add_spanning_tree(st) 203 | for each in st: 204 | self.assertTrue( 205 | (st[each], each) in gr.edges() or (each, st[each]) == (0, None) 206 | ) 207 | 208 | def test_add_empty_spanning_tree(self): 209 | gr = digraph() 210 | st = {} 211 | gr.add_spanning_tree(st) 212 | self.assertTrue(gr.nodes() == []) 213 | self.assertTrue(gr.edges() == []) 214 | 215 | def test_repr(self): 216 | """ 217 | Validate the repr string 218 | """ 219 | gr = testlib.new_graph() 220 | gr_repr = repr(gr) 221 | assert isinstance(gr_repr, str) 222 | assert gr.__class__.__name__ in gr_repr 223 | 224 | def test_order_len_equivlance(self): 225 | """ 226 | Verify the behavior of G.order() 227 | """ 228 | gr = testlib.new_graph() 229 | assert len(gr) == gr.order() 230 | assert gr.order() == len(gr.node_neighbors) 231 | 232 | def test_digraph_equality_nodes(self): 233 | """ 234 | Digraph equality test. This one checks node equality. 235 | """ 236 | gr = digraph() 237 | gr.add_nodes([0, 1, 2, 3, 4, 5]) 238 | 239 | gr2 = deepcopy(gr) 240 | 241 | gr3 = deepcopy(gr) 242 | gr3.del_node(5) 243 | 244 | gr4 = deepcopy(gr) 245 | gr4.add_node(6) 246 | gr4.del_node(0) 247 | 248 | assert gr == gr2 249 | assert gr2 == gr 250 | assert gr != gr3 251 | assert gr3 != gr 252 | assert gr != gr4 253 | assert gr4 != gr 254 | 255 | def test_digraph_equality_edges(self): 256 | """ 257 | Digraph equality test. This one checks edge equality. 258 | """ 259 | gr = digraph() 260 | gr.add_nodes([0, 1, 2, 3, 4]) 261 | gr.add_edge((0, 1), wt=1) 262 | gr.add_edge((0, 2), wt=2) 263 | gr.add_edge((1, 2), wt=3) 264 | gr.add_edge((3, 4), wt=4) 265 | 266 | gr2 = deepcopy(gr) 267 | 268 | gr3 = deepcopy(gr) 269 | gr3.del_edge((0, 2)) 270 | 271 | gr4 = deepcopy(gr) 272 | gr4.add_edge((2, 4)) 273 | 274 | gr5 = deepcopy(gr) 275 | gr5.del_edge((0, 2)) 276 | gr5.add_edge((2, 4)) 277 | 278 | gr6 = deepcopy(gr) 279 | gr6.del_edge((0, 2)) 280 | gr6.add_edge((0, 2), wt=10) 281 | 282 | assert gr == gr2 283 | assert gr2 == gr 284 | assert gr != gr3 285 | assert gr3 != gr 286 | assert gr != gr4 287 | assert gr4 != gr 288 | assert gr != gr5 289 | assert gr5 != gr 290 | assert gr != gr6 291 | assert gr6 != gr 292 | 293 | def test_digraph_equality_labels(self): 294 | """ 295 | Digraph equality test. This one checks node equality. 296 | """ 297 | gr = digraph() 298 | gr.add_nodes([0, 1, 2]) 299 | gr.add_edge((0, 1), label="l1") 300 | gr.add_edge((1, 2), label="l2") 301 | 302 | gr2 = deepcopy(gr) 303 | 304 | gr3 = deepcopy(gr) 305 | gr3.del_edge((0, 1)) 306 | gr3.add_edge((0, 1)) 307 | 308 | gr4 = deepcopy(gr) 309 | gr4.del_edge((0, 1)) 310 | gr4.add_edge((0, 1), label="l3") 311 | 312 | assert gr == gr2 313 | assert gr2 == gr 314 | assert gr != gr3 315 | assert gr3 != gr 316 | assert gr != gr4 317 | assert gr4 != gr 318 | 319 | def test_digraph_equality_attributes(self): 320 | """ 321 | Digraph equality test. This one checks node equality. 322 | """ 323 | gr = digraph() 324 | gr.add_nodes([0, 1, 2]) 325 | gr.add_edge((0, 1)) 326 | gr.add_node_attribute(1, ("a", "x")) 327 | gr.add_node_attribute(2, ("b", "y")) 328 | gr.add_edge_attribute((0, 1), ("c", "z")) 329 | 330 | gr2 = deepcopy(gr) 331 | 332 | gr3 = deepcopy(gr) 333 | gr3.del_edge((0, 1)) 334 | gr3.add_edge((0, 1)) 335 | 336 | gr4 = deepcopy(gr) 337 | gr4.del_edge((0, 1)) 338 | gr4.add_edge((0, 1)) 339 | gr4.add_edge_attribute((0, 1), ("d", "k")) 340 | 341 | gr5 = deepcopy(gr) 342 | gr5.del_node(2) 343 | gr5.add_node(2) 344 | gr5.add_node_attribute(0, ("d", "k")) 345 | 346 | assert gr == gr2 347 | assert gr2 == gr 348 | assert gr != gr3 349 | assert gr3 != gr 350 | assert gr != gr4 351 | assert gr4 != gr 352 | assert gr != gr5 353 | assert gr5 != gr 354 | 355 | 356 | if __name__ == "__main__": 357 | unittest.main() 358 | -------------------------------------------------------------------------------- /tests/unittests-filters.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Pedro Matiello 2 | # 3 | # Permission is hereby granted, free of charge, to any person 4 | # obtaining a copy of this software and associated documentation 5 | # files (the "Software"), to deal in the Software without 6 | # restriction, including without limitation the rights to use, 7 | # copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | # copies of the Software, and to permit persons to whom the 9 | # Software is furnished to do so, subject to the following 10 | # conditions: 11 | 12 | # The above copyright notice and this permission notice shall be 13 | # included in all copies or substantial portions of the Software. 14 | 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | # OTHER DEALINGS IN THE SOFTWARE. 23 | """ 24 | python-graph 25 | 26 | Unit tests for python-graph 27 | """ 28 | 29 | 30 | # Imports 31 | import unittest 32 | import pygraph 33 | from pygraph.algorithms.searching import depth_first_search, breadth_first_search 34 | from pygraph.classes.graph import graph 35 | 36 | from pygraph.algorithms.filters.radius import radius 37 | from pygraph.algorithms.filters.find import find 38 | from . import testlib 39 | 40 | 41 | class test_find_filter(unittest.TestCase): 42 | 43 | def test_bfs_in_empty_graph(self): 44 | gr = graph() 45 | st, lo = breadth_first_search(gr, filter=find(5)) 46 | assert st == {} 47 | assert lo == [] 48 | 49 | def test_bfs_in_graph(self): 50 | gr = testlib.new_graph() 51 | gr.add_node('find-me') 52 | gr.add_edge((0, 'find-me')) 53 | st, lo = breadth_first_search(gr, root=0, filter=find('find-me')) 54 | assert st['find-me'] == 0 55 | for each in st: 56 | assert st[each] == None or st[each] == 0 or st[st[each]] == 0 57 | 58 | def test_bfs_in_digraph(self): 59 | gr = testlib.new_digraph() 60 | gr.add_node('find-me') 61 | gr.add_edge((0, 'find-me')) 62 | st, lo = breadth_first_search(gr, root=0, filter=find('find-me')) 63 | assert st['find-me'] == 0 64 | for each in st: 65 | assert st[each] == None or st[each] == 0 or st[st[each]] == 0 66 | 67 | def test_dfs_in_empty_graph(self): 68 | gr = graph() 69 | st, pre, post = depth_first_search(gr) 70 | assert st == {} 71 | assert pre == [] 72 | assert post == [] 73 | 74 | def test_dfs_in_graph(self): 75 | gr = testlib.new_graph() 76 | gr.add_node('find-me') 77 | gr.add_node('dont-find-me') 78 | gr.add_edge((0, 'find-me')) 79 | gr.add_edge(('find-me','dont-find-me')) 80 | st, pre, post = depth_first_search(gr, root=0, filter=find('find-me')) 81 | assert st['find-me'] == 0 82 | assert 'dont-find-me' not in st 83 | 84 | def test_dfs_in_digraph(self): 85 | gr = testlib.new_digraph() 86 | gr.add_node('find-me') 87 | gr.add_node('dont-find-me') 88 | gr.add_edge((0, 'find-me')) 89 | gr.add_edge(('find-me','dont-find-me')) 90 | st, pre, post = depth_first_search(gr, root=0, filter=find('find-me')) 91 | assert st['find-me'] == 0 92 | assert 'dont-find-me' not in st 93 | 94 | 95 | class test_radius_filter(unittest.TestCase): 96 | 97 | def testbfs_in_empty_graph(self): 98 | gr = graph() 99 | st, lo = breadth_first_search(gr, filter=radius(2)) 100 | assert st == {} 101 | assert lo == [] 102 | 103 | def test_bfs_in_graph(self): 104 | gr = testlib.new_graph() 105 | st, lo = breadth_first_search(gr, root=0, filter=radius(3)) 106 | for each in st: 107 | assert (st[each] == None or st[each] == 0 108 | or st[st[each]] == 0 or st[st[st[each]]] == 0) 109 | 110 | def test_bfs_in_digraph(self): 111 | gr = testlib.new_digraph() 112 | st, lo = breadth_first_search(gr, root=0, filter=radius(3)) 113 | for each in st: 114 | assert (st[each] == None or st[each] == 0 115 | or st[st[each]] == 0 or st[st[st[each]]] == 0) 116 | 117 | def test_dfs_in_empty_graph(self): 118 | gr = graph() 119 | st, pre, post = depth_first_search(gr, filter=radius(2)) 120 | assert st == {} 121 | assert pre == [] 122 | assert post == [] 123 | 124 | def test_dfs_in_graph(self): 125 | gr = testlib.new_graph() 126 | st, pre, post = depth_first_search(gr, root=0, filter=radius(3)) 127 | for each in st: 128 | assert (st[each] == None or st[each] == 0 129 | or st[st[each]] == 0 or st[st[st[each]]] == 0) 130 | 131 | def test_dfs_in_digraph(self): 132 | gr = testlib.new_graph() 133 | st, pre, post = depth_first_search(gr, root=0, filter=radius(3)) 134 | for each in st: 135 | assert (st[each] == None or st[each] == 0 136 | or st[st[each]] == 0 or st[st[st[each]]] == 0) 137 | 138 | if __name__ == "__main__": 139 | unittest.main() -------------------------------------------------------------------------------- /tests/unittests-graph.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Pedro Matiello 2 | # 3 | # Permission is hereby granted, free of charge, to any person 4 | # obtaining a copy of this software and associated documentation 5 | # files (the "Software"), to deal in the Software without 6 | # restriction, including without limitation the rights to use, 7 | # copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | # copies of the Software, and to permit persons to whom the 9 | # Software is furnished to do so, subject to the following 10 | # conditions: 11 | 12 | # The above copyright notice and this permission notice shall be 13 | # included in all copies or substantial portions of the Software. 14 | 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | # OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | 25 | """ 26 | Unittests for graph.classes.Graph 27 | """ 28 | 29 | 30 | import unittest 31 | import pygraph 32 | from pygraph.algorithms.generators import generate 33 | from pygraph.classes.exceptions import AdditionError 34 | from pygraph.classes.graph import graph 35 | from . import testlib 36 | from copy import copy, deepcopy 37 | 38 | class test_graph(unittest.TestCase): 39 | 40 | # Add/Remove nodes and edges 41 | 42 | def test_raise_exception_on_duplicate_node_addition(self): 43 | gr = graph() 44 | gr.add_node('a_node') 45 | try: 46 | gr.add_node('a_node') 47 | except AdditionError: 48 | pass 49 | else: 50 | fail() 51 | 52 | def test_raise_exception_on_duplicate_edge_addition(self): 53 | gr = graph() 54 | gr.add_node('a_node') 55 | gr.add_node('other_node') 56 | gr.add_edge(("a_node","other_node")) 57 | try: 58 | gr.add_edge(("a_node","other_node")) 59 | except AdditionError: 60 | pass 61 | else: 62 | fail() 63 | 64 | def test_raise_exception_when_edge_added_from_non_existing_node(self): 65 | gr = graph() 66 | gr.add_nodes([0,1]) 67 | try: 68 | gr.add_edge((3,0)) 69 | except KeyError: 70 | pass 71 | else: 72 | fail() 73 | assert gr.node_neighbors == {0: set(), 1: set()} 74 | 75 | def test_raise_exception_when_edge_added_to_non_existing_node(self): 76 | gr = graph() 77 | gr.add_nodes([0,1]) 78 | try: 79 | gr.add_edge((0,3)) 80 | except KeyError: 81 | pass 82 | else: 83 | fail() 84 | assert gr.node_neighbors == {0: set(), 1: set()} 85 | 86 | def test_remove_node(self): 87 | gr = testlib.new_graph() 88 | gr.del_node(0) 89 | self.assertTrue(0 not in gr) 90 | for each, other in gr.edges(): 91 | self.assertTrue(each in gr) 92 | self.assertTrue(other in gr) 93 | 94 | def test_remove_edge_from_node_to_same_node(self): 95 | gr = graph() 96 | gr.add_node(0) 97 | gr.add_edge((0, 0)) 98 | gr.del_edge((0, 0)) 99 | 100 | def test_remove_node_with_edge_to_itself(self): 101 | gr = graph() 102 | gr.add_node(0) 103 | gr.add_edge((0, 0)) 104 | gr.del_node(0) 105 | 106 | def test_edges_between_different_nodes_should_be_arrows_in_both_ways(self): 107 | gr = graph() 108 | gr.add_nodes([0,1]) 109 | gr.add_edge((0,1), label="label", attrs=[('key','value')]) 110 | assert (0,1) in gr.edges() 111 | assert (1,0) in gr.edges() 112 | assert len(gr.edges()) == 2 113 | assert gr.neighbors(0) == [1] 114 | assert gr.neighbors(1) == [0] 115 | assert (0,1) in list(gr.edge_properties.keys()) 116 | assert (1,0) in list(gr.edge_properties.keys()) 117 | assert (0,1) in list(gr.edge_attr.keys()) 118 | assert (1,0) in list(gr.edge_attr.keys()) 119 | 120 | def test_edges_between_different_nodes_should_be_a_single_arrow(self): 121 | gr = graph() 122 | gr.add_node(0) 123 | gr.add_edge((0,0), label="label", attrs=[('key','value')]) 124 | assert (0,0) in gr.edges() 125 | assert len(gr.edges()) == 1 126 | assert gr.neighbors(0) == [0] 127 | assert (0,0) in list(gr.edge_properties.keys()) 128 | assert (0,0) in list(gr.edge_attr.keys()) 129 | assert len(gr.edge_attr[(0,0)]) == 1 130 | 131 | 132 | # Invert graph 133 | 134 | def test_invert_graph(self): 135 | gr = testlib.new_graph() 136 | inv = gr.inverse() 137 | for each in gr.edges(): 138 | self.assertTrue(each not in inv.edges()) 139 | for each in inv.edges(): 140 | self.assertTrue(each not in gr.edges()) 141 | 142 | def test_invert_empty_graph(self): 143 | gr = graph() 144 | inv = gr.inverse() 145 | self.assertTrue(gr.nodes() == []) 146 | self.assertTrue(gr.edges() == []) 147 | 148 | 149 | # Complete graph 150 | 151 | def test_complete_graph(self): 152 | gr = graph() 153 | gr.add_nodes(list(range(10))) 154 | gr.complete() 155 | for i in range(10): 156 | for j in range(10): 157 | self.assertTrue((i, j) in gr.edges() or i == j) 158 | 159 | def test_complete_empty_graph(self): 160 | gr = graph() 161 | gr.complete() 162 | self.assertTrue(gr.nodes() == []) 163 | self.assertTrue(gr.edges() == []) 164 | 165 | def test_complete_graph_with_one_node(self): 166 | gr = graph() 167 | gr.add_node(0) 168 | gr.complete() 169 | self.assertTrue(gr.nodes() == [0]) 170 | self.assertTrue(gr.edges() == []) 171 | 172 | 173 | # Add graph 174 | 175 | def test_add_graph(self): 176 | gr1 = testlib.new_graph() 177 | gr2 = testlib.new_graph() 178 | gr1.add_graph(gr2) 179 | for each in gr2.nodes(): 180 | self.assertTrue(each in gr1) 181 | for each in gr2.edges(): 182 | self.assertTrue(each in gr1.edges()) 183 | 184 | def test_add_empty_graph(self): 185 | gr1 = testlib.new_graph() 186 | gr1c = copy(gr1) 187 | gr2 = graph() 188 | gr1.add_graph(gr2) 189 | self.assertTrue(gr1.nodes() == gr1c.nodes()) 190 | self.assertTrue(gr1.edges() == gr1c.edges()) 191 | 192 | 193 | # Add spanning tree 194 | 195 | def test_add_spanning_tree(self): 196 | gr = graph() 197 | st = {0: None, 1: 0, 2:0, 3: 1, 4: 2, 5: 3} 198 | gr.add_spanning_tree(st) 199 | for each in st: 200 | self.assertTrue((each, st[each]) in gr.edges() or (each, st[each]) == (0, None)) 201 | self.assertTrue((st[each], each) in gr.edges() or (each, st[each]) == (0, None)) 202 | 203 | def test_add_empty_spanning_tree(self): 204 | gr = graph() 205 | st = {} 206 | gr.add_spanning_tree(st) 207 | self.assertTrue(gr.nodes() == []) 208 | self.assertTrue(gr.edges() == []) 209 | 210 | def test_repr(self): 211 | """ 212 | Validate the repr string 213 | """ 214 | gr = testlib.new_graph() 215 | gr_repr = repr(gr) 216 | assert isinstance(gr_repr, str ) 217 | assert gr.__class__.__name__ in gr_repr 218 | 219 | def test_order_len_equivlance(self): 220 | """ 221 | Verify the behavior of G.order() 222 | """ 223 | gr = testlib.new_graph() 224 | assert len(gr) == gr.order() 225 | assert gr.order() == len( gr.node_neighbors ) 226 | 227 | def test_graph_equality_nodes(self): 228 | """ 229 | Graph equality test. This one checks node equality. 230 | """ 231 | gr = graph() 232 | gr.add_nodes([0,1,2,3,4,5]) 233 | 234 | gr2 = deepcopy(gr) 235 | 236 | gr3 = deepcopy(gr) 237 | gr3.del_node(5) 238 | 239 | gr4 = deepcopy(gr) 240 | gr4.add_node(6) 241 | gr4.del_node(0) 242 | 243 | assert gr == gr2 244 | assert gr2 == gr 245 | assert gr != gr3 246 | assert gr3 != gr 247 | assert gr != gr4 248 | assert gr4 != gr 249 | 250 | def test_graph_equality_edges(self): 251 | """ 252 | Graph equality test. This one checks edge equality. 253 | """ 254 | gr = graph() 255 | gr.add_nodes([0,1,2,3,4]) 256 | gr.add_edge((0,1), wt=1) 257 | gr.add_edge((0,2), wt=2) 258 | gr.add_edge((1,2), wt=3) 259 | gr.add_edge((3,4), wt=4) 260 | 261 | gr2 = deepcopy(gr) 262 | 263 | gr3 = deepcopy(gr) 264 | gr3.del_edge((0,2)) 265 | 266 | gr4 = deepcopy(gr) 267 | gr4.add_edge((2,4)) 268 | 269 | gr5 = deepcopy(gr) 270 | gr5.del_edge((0,2)) 271 | gr5.add_edge((2,4)) 272 | 273 | gr6 = deepcopy(gr) 274 | gr6.del_edge((0,2)) 275 | gr6.add_edge((0,2), wt=10) 276 | 277 | assert gr == gr2 278 | assert gr2 == gr 279 | assert gr != gr3 280 | assert gr3 != gr 281 | assert gr != gr4 282 | assert gr4 != gr 283 | assert gr != gr5 284 | assert gr5 != gr 285 | assert gr != gr6 286 | assert gr6 != gr 287 | 288 | def test_graph_equality_labels(self): 289 | """ 290 | Graph equality test. This one checks node equality. 291 | """ 292 | gr = graph() 293 | gr.add_nodes([0,1,2]) 294 | gr.add_edge((0,1), label="l1") 295 | gr.add_edge((1,2), label="l2") 296 | 297 | gr2 = deepcopy(gr) 298 | 299 | gr3 = deepcopy(gr) 300 | gr3.del_edge((0,1)) 301 | gr3.add_edge((0,1)) 302 | 303 | gr4 = deepcopy(gr) 304 | gr4.del_edge((0,1)) 305 | gr4.add_edge((0,1), label="l3") 306 | 307 | assert gr == gr2 308 | assert gr2 == gr 309 | assert gr != gr3 310 | assert gr3 != gr 311 | assert gr != gr4 312 | assert gr4 != gr 313 | 314 | def test_graph_equality_attributes(self): 315 | """ 316 | Graph equality test. This one checks node equality. 317 | """ 318 | gr = graph() 319 | gr.add_nodes([0,1,2]) 320 | gr.add_edge((0,1)) 321 | gr.add_node_attribute(1, ('a','x')) 322 | gr.add_node_attribute(2, ('b','y')) 323 | gr.add_edge_attribute((0,1), ('c','z')) 324 | 325 | gr2 = deepcopy(gr) 326 | 327 | gr3 = deepcopy(gr) 328 | gr3.del_edge((0,1)) 329 | gr3.add_edge((0,1)) 330 | 331 | gr4 = deepcopy(gr) 332 | gr4.del_edge((0,1)) 333 | gr4.add_edge((0,1)) 334 | gr4.add_edge_attribute((0,1), ('d','k')) 335 | 336 | gr5 = deepcopy(gr) 337 | gr5.del_node(2) 338 | gr5.add_node(2) 339 | gr5.add_node_attribute(0, ('d','k')) 340 | 341 | assert gr == gr2 342 | assert gr2 == gr 343 | assert gr != gr3 344 | assert gr3 != gr 345 | assert gr != gr4 346 | assert gr4 != gr 347 | assert gr != gr5 348 | assert gr5 != gr 349 | 350 | if __name__ == "__main__": 351 | unittest.main() -------------------------------------------------------------------------------- /tests/unittests-heuristics.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Pedro Matiello 2 | # 3 | # Permission is hereby granted, free of charge, to any person 4 | # obtaining a copy of this software and associated documentation 5 | # files (the "Software"), to deal in the Software without 6 | # restriction, including without limitation the rights to use, 7 | # copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | # copies of the Software, and to permit persons to whom the 9 | # Software is furnished to do so, subject to the following 10 | # conditions: 11 | 12 | # The above copyright notice and this permission notice shall be 13 | # included in all copies or substantial portions of the Software. 14 | 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | # OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | 25 | """ 26 | Unittests for graph.algorithms.heuristics 27 | """ 28 | 29 | 30 | import unittest 31 | import pygraph 32 | from pygraph.classes.graph import graph 33 | from pygraph.classes.digraph import digraph 34 | from pygraph.algorithms.heuristics.euclidean import euclidean 35 | from pygraph.algorithms.heuristics.chow import chow 36 | from pygraph.classes import exceptions 37 | 38 | from .test_data import nations_of_the_world 39 | 40 | 41 | class test_chow(unittest.TestCase): 42 | 43 | def setUp(self): 44 | self.G = graph() 45 | nations_of_the_world(self.G) 46 | 47 | def test_basic(self): 48 | """ 49 | Test some very basic functionality 50 | """ 51 | englands_neighbors = self.G.neighbors("England") 52 | assert set(['Wales', 'Scotland', 'France', 'Ireland']) == set( englands_neighbors ) 53 | 54 | def test_chow(self): 55 | heuristic = chow( "Wales", "North Korea", "Russia" ) 56 | heuristic.optimize(self.G) 57 | result = pygraph.algorithms.minmax.heuristic_search( self.G, "England", "India", heuristic ) 58 | 59 | def test_chow_unreachable(self): 60 | heuristic = chow( "Wales", "North Korea", "Russia" ) 61 | self.G.add_node("Sealand") 62 | self.G.add_edge(("England", "Sealand")) 63 | heuristic.optimize(self.G) 64 | self.G.del_edge(("England", "Sealand")) 65 | 66 | try: 67 | result = pygraph.algorithms.minmax.heuristic_search( self.G, "England", "Sealand" , heuristic ) 68 | except exceptions.NodeUnreachable as _: 69 | return 70 | 71 | assert False, "This test should raise an unreachable error." 72 | 73 | 74 | class test_euclidean(unittest.TestCase): 75 | 76 | def setUp(self): 77 | self.G = pygraph.classes.graph.graph() 78 | self.G.add_node('A', [('position',[0,0])]) 79 | self.G.add_node('B', [('position',[2,0])]) 80 | self.G.add_node('C', [('position',[2,3])]) 81 | self.G.add_node('D', [('position',[1,2])]) 82 | self.G.add_edge(('A', 'B'), wt=4) 83 | self.G.add_edge(('A', 'D'), wt=5) 84 | self.G.add_edge(('B', 'C'), wt=9) 85 | self.G.add_edge(('D', 'C'), wt=2) 86 | 87 | def test_euclidean(self): 88 | heuristic = euclidean() 89 | heuristic.optimize(self.G) 90 | result = pygraph.algorithms.minmax.heuristic_search(self.G, 'A', 'C', heuristic ) 91 | assert result == ['A', 'D', 'C'] 92 | 93 | if __name__ == "__main__": 94 | unittest.main() -------------------------------------------------------------------------------- /tests/unittests-hypergraph.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Pedro Matiello 2 | # 3 | # Permission is hereby granted, free of charge, to any person 4 | # obtaining a copy of this software and associated documentation 5 | # files (the "Software"), to deal in the Software without 6 | # restriction, including without limitation the rights to use, 7 | # copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | # copies of the Software, and to permit persons to whom the 9 | # Software is furnished to do so, subject to the following 10 | # conditions: 11 | 12 | # The above copyright notice and this permission notice shall be 13 | # included in all copies or substantial portions of the Software. 14 | 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | # OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | 25 | """ 26 | Unittests for graph.classes.hypergraph 27 | """ 28 | 29 | import unittest 30 | from pygraph.classes.exceptions import AdditionError 31 | from pygraph.classes.hypergraph import hypergraph 32 | from . import testlib 33 | from copy import deepcopy 34 | 35 | 36 | class test_hypergraph(unittest.TestCase): 37 | # Add/Remove nodes and edges 38 | 39 | def test_raise_exception_on_duplicate_node_addition(self): 40 | gr = hypergraph() 41 | gr.add_node("a_node") 42 | try: 43 | gr.add_node("a_node") 44 | except AdditionError: 45 | pass 46 | else: 47 | self.fail() 48 | 49 | def test_raise_exception_on_duplicate_edge_link(self): 50 | gr = hypergraph() 51 | gr.add_node("a node") 52 | gr.add_hyperedge("an edge") 53 | gr.link("a node", "an edge") 54 | try: 55 | gr.link("a node", "an edge") 56 | except AdditionError: 57 | pass 58 | else: 59 | self.fail() 60 | 61 | def test_raise_exception_on_non_existing_link_removal(self): 62 | gr = hypergraph() 63 | gr.add_node(0) 64 | gr.add_hyperedge(1) 65 | try: 66 | gr.unlink(0, 1) 67 | except ValueError: 68 | pass 69 | else: 70 | self.fail() 71 | 72 | def test_raise_exception_when_edge_added_from_non_existing_node(self): 73 | gr = hypergraph() 74 | gr.add_nodes([0, 1]) 75 | try: 76 | gr.link(3, 0) 77 | except KeyError: 78 | pass 79 | else: 80 | self.fail() 81 | assert gr.neighbors(0) == [] 82 | 83 | def test_raise_exception_when_edge_added_to_non_existing_node(self): 84 | gr = hypergraph() 85 | gr.add_nodes([0, 1]) 86 | try: 87 | gr.link(0, 3) 88 | except KeyError: 89 | pass 90 | else: 91 | self.fail() 92 | assert gr.neighbors(0) == [] 93 | 94 | def test_remove_node(self): 95 | gr = testlib.new_hypergraph() 96 | gr.del_node(0) 97 | self.assertTrue(0 not in gr.nodes()) 98 | for e in gr.hyperedges(): 99 | for n in gr.links(e): 100 | self.assertTrue(n in gr.nodes()) 101 | 102 | def test_remove_edge(self): 103 | h = hypergraph() 104 | h.add_nodes([1, 2]) 105 | h.add_edges(["a", "b"]) 106 | 107 | h.link(1, "a") 108 | h.link(2, "a") 109 | h.link(1, "b") 110 | h.link(2, "b") 111 | 112 | # Delete an edge 113 | h.del_edge("a") 114 | 115 | assert 1 == len(h.hyperedges()) 116 | 117 | gr = testlib.new_hypergraph() 118 | edge_no = len(gr.nodes()) + 1 119 | gr.del_hyperedge(edge_no) 120 | self.assertTrue(edge_no not in gr.hyperedges()) 121 | 122 | def test_remove_link_from_node_to_same_node(self): 123 | gr = hypergraph() 124 | gr.add_node(0) 125 | gr.add_hyperedge(0) 126 | gr.link(0, 0) 127 | gr.unlink(0, 0) 128 | 129 | def test_remove_node_with_edge_to_itself(self): 130 | gr = hypergraph() 131 | gr.add_node(0) 132 | gr.add_hyperedge(0) 133 | gr.link(0, 0) 134 | gr.del_node(0) 135 | 136 | def test_check_add_node_s(self): 137 | gr = hypergraph() 138 | nodes = [1, 2, 3] 139 | gr.add_nodes(nodes) 140 | gr.add_node(0) 141 | 142 | for n in [0] + nodes: 143 | assert n in gr 144 | assert gr.has_node(n) 145 | 146 | def test_rank(self): 147 | # Uniform case 148 | gr = testlib.new_uniform_hypergraph(3) 149 | assert 3 == gr.rank() 150 | 151 | # Non-uniform case 152 | gr = testlib.new_hypergraph() 153 | num = max([len(gr.links(e)) for e in gr.hyperedges()]) 154 | assert num == gr.rank() 155 | 156 | def test_repr(self): 157 | """ 158 | Validate the repr string 159 | """ 160 | gr = testlib.new_hypergraph() 161 | gr_repr = repr(gr) 162 | assert isinstance(gr_repr, str) 163 | assert gr.__class__.__name__ in gr_repr 164 | 165 | def test_order_len_equivlance(self): 166 | """ 167 | Verify the behavior of G.order() 168 | """ 169 | gr = testlib.new_hypergraph() 170 | assert len(gr) == gr.order() 171 | assert gr.order() == len(gr.node_links) 172 | 173 | def test_hypergraph_equality_nodes(self): 174 | """ 175 | Hyperaph equality test. This one checks node equality. 176 | """ 177 | gr = hypergraph() 178 | gr.add_nodes([0, 1, 2, 3, 4, 5]) 179 | 180 | gr2 = deepcopy(gr) 181 | 182 | gr3 = deepcopy(gr) 183 | gr3.del_node(5) 184 | 185 | gr4 = deepcopy(gr) 186 | gr4.add_node(6) 187 | gr4.del_node(0) 188 | 189 | assert gr == gr2 190 | assert gr2 == gr 191 | assert gr != gr3 192 | assert gr3 != gr 193 | assert gr != gr4 194 | assert gr4 != gr 195 | 196 | def test_hypergraph_equality_edges(self): 197 | """ 198 | Hyperaph equality test. This one checks edge equality. 199 | """ 200 | gr = hypergraph() 201 | gr.add_nodes([0, 1, 2, 3]) 202 | gr.add_edge("e1") 203 | gr.add_edge("e2") 204 | gr.link(0, "e1") 205 | gr.link(1, "e1") 206 | gr.link(1, "e2") 207 | gr.link(2, "e2") 208 | 209 | gr2 = deepcopy(gr) 210 | 211 | gr3 = deepcopy(gr) 212 | gr3.del_edge("e2") 213 | 214 | gr4 = deepcopy(gr) 215 | gr4.unlink(1, "e2") 216 | 217 | assert gr == gr2 218 | assert gr2 == gr 219 | assert gr != gr3 220 | assert gr3 != gr 221 | assert gr != gr4 222 | assert gr4 != gr 223 | 224 | def test_hypergraph_equality_labels(self): 225 | """ 226 | Hyperaph equality test. This one checks edge equality. 227 | """ 228 | gr = hypergraph() 229 | gr.add_nodes([0, 1, 2, 3]) 230 | gr.add_edge("e1") 231 | gr.add_edge("e2") 232 | gr.add_edge("e3") 233 | gr.set_edge_label("e1", "l1") 234 | gr.set_edge_label("e2", "l2") 235 | 236 | gr2 = deepcopy(gr) 237 | 238 | gr3 = deepcopy(gr) 239 | gr3.set_edge_label("e3", "l3") 240 | 241 | gr4 = deepcopy(gr) 242 | gr4.set_edge_label("e1", "lx") 243 | 244 | gr5 = deepcopy(gr) 245 | gr5.del_edge("e1") 246 | gr5.add_edge("e1") 247 | 248 | assert gr == gr2 249 | assert gr2 == gr 250 | assert gr != gr3 251 | assert gr3 != gr 252 | assert gr != gr4 253 | assert gr4 != gr 254 | assert gr != gr5 255 | assert gr5 != gr 256 | 257 | def test_hypergraph_equality_attributes(self): 258 | """ 259 | Hyperaph equality test. This one checks edge equality. 260 | """ 261 | gr = hypergraph() 262 | gr.add_nodes([0, 1]) 263 | gr.add_edge("e1") 264 | gr.add_edge("e2") 265 | gr.add_node_attribute(0, ("a", 0)) 266 | gr.add_edge_attribute("e1", ("b", 1)) 267 | 268 | gr2 = deepcopy(gr) 269 | 270 | gr3 = deepcopy(gr) 271 | gr3.add_node_attribute(0, ("x", "y")) 272 | 273 | gr4 = deepcopy(gr) 274 | gr4.add_edge_attribute("e1", ("u", "v")) 275 | 276 | gr5 = deepcopy(gr) 277 | gr5.del_edge("e1") 278 | gr5.add_edge("e1") 279 | 280 | gr6 = deepcopy(gr) 281 | gr6.del_node(0) 282 | gr6.add_node(0) 283 | 284 | assert gr == gr2 285 | assert gr2 == gr 286 | assert gr != gr3 287 | assert gr3 != gr 288 | assert gr != gr4 289 | assert gr4 != gr 290 | assert gr != gr5 291 | assert gr5 != gr 292 | assert gr != gr6 293 | assert gr6 != gr 294 | 295 | def test_hypergraph_equality_weight(self): 296 | """ 297 | Hyperaph equality test. This one checks edge equality. 298 | """ 299 | gr = hypergraph() 300 | gr.add_nodes([0, 1, 2, 3]) 301 | gr.add_edge("e1") 302 | gr.add_edge("e2") 303 | gr.add_edge("e3") 304 | gr.set_edge_weight("e1", 2) 305 | 306 | gr2 = deepcopy(gr) 307 | 308 | gr3 = deepcopy(gr) 309 | gr3.set_edge_weight("e3", 2) 310 | 311 | gr4 = deepcopy(gr) 312 | gr4.set_edge_weight("e1", 1) 313 | 314 | assert gr == gr2 315 | assert gr2 == gr 316 | assert gr != gr3 317 | assert gr3 != gr 318 | assert gr != gr4 319 | assert gr4 != gr 320 | 321 | def test_hypergraph_link_unlink_link(self): 322 | """ 323 | Hypergraph link-unlink-link test. It makes sure that unlink cleans 324 | everything properly. No AdditionError should occur. 325 | """ 326 | h = hypergraph() 327 | h.add_nodes([1, 2]) 328 | h.add_edges(["e1"]) 329 | 330 | h.link(1, "e1") 331 | h.unlink(1, "e1") 332 | h.link(1, "e1") 333 | 334 | 335 | if __name__ == "__main__": 336 | unittest.main() 337 | -------------------------------------------------------------------------------- /tests/unittests-minmax.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Pedro Matiello 2 | # Johannes Reinhardt 3 | # 4 | # Permission is hereby granted, free of charge, to any person 5 | # obtaining a copy of this software and associated documentation 6 | # files (the "Software"), to deal in the Software without 7 | # restriction, including without limitation the rights to use, 8 | # copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the 10 | # Software is furnished to do so, subject to the following 11 | # conditions: 12 | 13 | # The above copyright notice and this permission notice shall be 14 | # included in all copies or substantial portions of the Software. 15 | 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 18 | # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 20 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 21 | # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 23 | # OTHER DEALINGS IN THE SOFTWARE. 24 | 25 | 26 | """ 27 | Unittests for graph.algorithms.searching 28 | """ 29 | 30 | import unittest 31 | from . import testlib 32 | 33 | from pygraph.classes.graph import graph 34 | from pygraph.classes.digraph import digraph 35 | 36 | from pygraph.algorithms.searching import depth_first_search 37 | from pygraph.algorithms.minmax import minimal_spanning_tree_kruskal,\ 38 | minimal_spanning_tree_prim, shortest_path, heuristic_search, shortest_path_bellman_ford, maximum_flow, cut_tree 39 | from pygraph.algorithms.heuristics.chow import chow 40 | from pygraph.classes.exceptions import NegativeWeightCycleError 41 | 42 | from copy import deepcopy 43 | 44 | # helpers 45 | 46 | def tree_weight(gr, tree): 47 | sum = 0; 48 | for each in tree: 49 | sum = sum + gr.edge_weight((each, tree[each])) 50 | return sum 51 | 52 | def add_spanning_tree(gr, st): 53 | # A very tolerant implementation. 54 | gr.add_nodes(list(st.keys())) 55 | for each in st: 56 | if ((st[each] is not None) and (not gr.has_edge((st[each], each)))): # Accepts invalid STs 57 | gr.add_edge((st[each], each)) 58 | 59 | def bf_path(gr, root, target, remainder): 60 | if (remainder <= 0): return True 61 | if (root == target): return False 62 | for each in gr[root]: 63 | if (not bf_path(gr, each, target, remainder - gr.edge_weight((root, each)))): 64 | return False 65 | return True 66 | 67 | def generate_fixture_digraph(): 68 | #helper for bellman-ford algorithm 69 | G = digraph() 70 | G.add_nodes([1,2,3,4,5]) 71 | G.add_edge((1,2), 6) 72 | G.add_edge((1,4), 7) 73 | G.add_edge((2,4), 8) 74 | G.add_edge((3,2), -2) 75 | G.add_edge((4,3), -3) 76 | G.add_edge((2,5), -4) 77 | G.add_edge((4,5), 9) 78 | G.add_edge((5,1), 2) 79 | G.add_edge((5,3), 7) 80 | return G 81 | 82 | def generate_fixture_digraph_neg_weight_cycle(): 83 | #graph with a neg. weight cycle 84 | G = generate_fixture_digraph() 85 | G.del_edge((2,4)) 86 | G.add_edge((2,4), 2)#changed 87 | 88 | G.add_nodes([100,200]) #unconnected part 89 | G.add_edge((100,200),2) 90 | return G 91 | 92 | def generate_fixture_digraph_unconnected(): 93 | G = generate_fixture_digraph() 94 | G.add_nodes([100,200]) 95 | G.add_edge((100,200),2) 96 | return G 97 | 98 | # minimal spanning tree tests 99 | 100 | class test_minimal_spanning_tree_kruskal(unittest.TestCase): 101 | 102 | def test_minimal_spanning_tree_kruskal_on_graph(self): 103 | gr = testlib.new_graph(wt_range=(1,10)) 104 | mst = minimal_spanning_tree_kruskal(gr, root=0) 105 | print(mst) 106 | wt = tree_weight(gr, mst) 107 | len_dfs = len(depth_first_search(gr, root=0)[0]) 108 | for each in mst: 109 | if (mst[each] != None): 110 | mst_copy = deepcopy(mst) 111 | del(mst_copy[each]) 112 | for other in gr[each]: 113 | mst_copy[each] = other 114 | if (tree_weight(gr, mst_copy) < wt): 115 | gr2 = graph() 116 | add_spanning_tree(gr2, mst_copy) 117 | assert len(depth_first_search(gr2, root=0)[0]) < len_dfs 118 | 119 | 120 | class test_minimal_spanning_tree_prim(unittest.TestCase): 121 | 122 | def test_minimal_spanning_tree_prim_on_graph(self): 123 | gr = testlib.new_graph(wt_range=(1,10)) 124 | mst = minimal_spanning_tree_prim(gr, root=0) 125 | wt = tree_weight(gr, mst) 126 | len_dfs = len(depth_first_search(gr, root=0)[0]) 127 | for each in mst: 128 | if (mst[each] != None): 129 | mst_copy = deepcopy(mst) 130 | del(mst_copy[each]) 131 | for other in gr[each]: 132 | mst_copy[each] = other 133 | if (tree_weight(gr, mst_copy) < wt): 134 | gr2 = graph() 135 | add_spanning_tree(gr2, mst_copy) 136 | assert len(depth_first_search(gr2, root=0)[0]) < len_dfs 137 | 138 | 139 | 140 | # shortest path tests 141 | 142 | class test_shortest_path(unittest.TestCase): 143 | 144 | def test_shortest_path_on_graph(self): 145 | gr = testlib.new_graph(wt_range=(1,10)) 146 | st, dist = shortest_path(gr, 0) 147 | for each in gr: 148 | if (each in dist): 149 | assert bf_path(gr, 0, each, dist[each]) 150 | 151 | def test_shortest_path_on_digraph(self): 152 | # Test stub: not checking for correctness yet 153 | gr = testlib.new_digraph(wt_range=(1,10)) 154 | st, dist = shortest_path(gr, 0) 155 | for each in gr: 156 | if (each in dist): 157 | assert bf_path(gr, 0, each, dist[each]) 158 | 159 | def test_shortest_path_should_fail_if_source_does_not_exist(self): 160 | gr = testlib.new_graph() 161 | try: 162 | shortest_path(gr, 'invalid') 163 | assert False 164 | except (KeyError): 165 | pass 166 | 167 | class test_shortest_path_bellman_ford(unittest.TestCase): 168 | 169 | def test_shortest_path_BF_on_empty_digraph(self): 170 | pre, dist = shortest_path_bellman_ford(digraph(), 1) 171 | assert pre == {1:None} and dist == {1:0} 172 | 173 | def test_shortest_path_BF_on_digraph(self): 174 | #testing correctness on the fixture 175 | gr = generate_fixture_digraph() 176 | pre,dist = shortest_path_bellman_ford(gr, 1) 177 | assert pre == {1: None, 2: 3, 3: 4, 4: 1, 5: 2} \ 178 | and dist == {1: 0, 2: 2, 3: 4, 4: 7, 5: -2} 179 | 180 | def test_shortest_path_BF_on_digraph_with_negwcycle(self): 181 | #test negative weight cycle detection 182 | gr = generate_fixture_digraph_neg_weight_cycle() 183 | self.assertRaises(NegativeWeightCycleError, 184 | shortest_path_bellman_ford, gr, 1) 185 | 186 | def test_shortest_path_BF_on_unconnected_graph(self): 187 | gr = generate_fixture_digraph_unconnected() 188 | pre,dist = shortest_path_bellman_ford(gr, 100) 189 | assert pre == {200: 100, 100: None} and \ 190 | dist == {200: 2, 100: 0} 191 | 192 | class test_maxflow_mincut(unittest.TestCase): 193 | 194 | def test_trivial_maxflow(self): 195 | gr = digraph() 196 | gr.add_nodes([0,1,2,3]) 197 | gr.add_edge((0,1), wt=5) 198 | gr.add_edge((1,2), wt=3) 199 | gr.add_edge((2,3), wt=7) 200 | flows, cuts = maximum_flow(gr, 0, 3) 201 | assert flows[(0,1)] == 3 202 | assert flows[(1,2)] == 3 203 | assert flows[(2,3)] == 3 204 | 205 | def test_random_maxflow(self): 206 | gr = testlib.new_digraph(wt_range=(1,20)) 207 | flows, cuts = maximum_flow(gr, 0, 1) 208 | # Sanity test 209 | for each in flows: 210 | assert gr.edge_weight(each) >= flows[each] 211 | 212 | # Tests for heuristic search are not necessary here as it's tested 213 | # in unittests-heuristics.py 214 | 215 | class test_cut_tree(unittest.TestCase): 216 | 217 | def test_cut_tree(self): 218 | #set up the graph (see example on wikipedia page for Gomory-Hu tree) 219 | gr = graph() 220 | gr.add_nodes([0,1,2,3,4,5]) 221 | gr.add_edge((0,1), wt=1) 222 | gr.add_edge((0,2), wt=7) 223 | gr.add_edge((1,3), wt=3) 224 | gr.add_edge((1,2), wt=1) 225 | gr.add_edge((1,4), wt=2) 226 | gr.add_edge((2,4), wt=4) 227 | gr.add_edge((3,4), wt=1) 228 | gr.add_edge((3,5), wt=6) 229 | gr.add_edge((4,5), wt=2) 230 | 231 | ct = cut_tree(gr) 232 | 233 | #check ct 234 | assert ct[(2,0)] == 8 235 | assert ct[(4,2)] == 6 236 | assert ct[(1,4)] == 7 237 | assert ct[(3,1)] == 6 238 | assert ct[(5,3)] == 8 239 | 240 | def test_cut_tree_with_empty_graph(self): 241 | gr = graph() 242 | ct = cut_tree(gr) 243 | assert ct == {} 244 | 245 | def test_cut_tree_with_random_graph(self): 246 | gr = testlib.new_graph() 247 | ct = cut_tree(gr) 248 | 249 | 250 | if __name__ == "__main__": 251 | unittest.main() 252 | -------------------------------------------------------------------------------- /tests/unittests-pagerank.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2010 Pedro Matiello 2 | # Juarez Bochi 3 | # 4 | # Permission is hereby granted, free of charge, to any person 5 | # obtaining a copy of this software and associated documentation 6 | # files (the "Software"), to deal in the Software without 7 | # restriction, including without limitation the rights to use, 8 | # copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the 10 | # Software is furnished to do so, subject to the following 11 | # conditions: 12 | 13 | # The above copyright notice and this permission notice shall be 14 | # included in all copies or substantial portions of the Software. 15 | 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 18 | # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 20 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 21 | # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 23 | # OTHER DEALINGS IN THE SOFTWARE. 24 | 25 | 26 | """ 27 | Unittests for pygraph.algorithms.pagerank 28 | """ 29 | 30 | import unittest 31 | from pygraph.classes.digraph import digraph 32 | from pygraph.algorithms.pagerank import pagerank 33 | from . import testlib 34 | 35 | class test_pagerank(unittest.TestCase): 36 | 37 | # pagerank algorithm 38 | 39 | def test_pagerank_empty(self): 40 | #Test if an empty dict is returned for an empty graph 41 | G = digraph() 42 | self.assertEqual(pagerank(G), {}) 43 | 44 | def test_pagerank_cycle(self): 45 | #Test if all nodes in a cycle graph have the same value 46 | G = digraph() 47 | G.add_nodes([1, 2, 3, 4, 5]) 48 | G.add_edge((1, 2)) 49 | G.add_edge((2, 3)) 50 | G.add_edge((3, 4)) 51 | G.add_edge((4, 5)) 52 | G.add_edge((5, 1)) 53 | self.assertEqual(pagerank(G), {1: 0.2, 2: 0.2, 3: 0.2, 4: 0.2, 5: 0.2}) 54 | 55 | def test_pagerank(self): 56 | #Test example from wikipedia: http://en.wikipedia.org/wiki/File:Linkstruct3.svg 57 | G = digraph() 58 | G.add_nodes([1, 2, 3, 4, 5, 6, 7]) 59 | G.add_edge((1, 2)) 60 | G.add_edge((1, 3)) 61 | G.add_edge((1, 4)) 62 | G.add_edge((1, 5)) 63 | G.add_edge((1, 7)) 64 | G.add_edge((2, 1)) 65 | G.add_edge((3, 1)) 66 | G.add_edge((3, 2)) 67 | G.add_edge((4, 2)) 68 | G.add_edge((4, 3)) 69 | G.add_edge((4, 5)) 70 | G.add_edge((5, 1)) 71 | G.add_edge((5, 3)) 72 | G.add_edge((5, 4)) 73 | G.add_edge((5, 6)) 74 | G.add_edge((6, 1)) 75 | G.add_edge((6, 5)) 76 | G.add_edge((7, 5)) 77 | expected_pagerank = { 78 | 1: 0.280, 79 | 2: 0.159, 80 | 3: 0.139, 81 | 4: 0.108, 82 | 5: 0.184, 83 | 6: 0.061, 84 | 7: 0.069, 85 | } 86 | pr = pagerank(G) 87 | for k in pr: 88 | self.assertAlmostEqual(pr[k], expected_pagerank[k], places=3) 89 | 90 | def test_pagerank_random(self): 91 | G = testlib.new_digraph() 92 | md = 0.00001 93 | df = 0.85 94 | pr = pagerank(G, damping_factor=df, min_delta=md) 95 | min_value = (1.0-df)/len(G) 96 | for node in G: 97 | expected = min_value 98 | for each in G.incidents(node): 99 | expected += (df * pr[each] / len(G.neighbors(each))) 100 | assert abs(pr[node] - expected) < md 101 | 102 | if __name__ == "__main__": 103 | unittest.main() 104 | -------------------------------------------------------------------------------- /tests/unittests-readwrite.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Pedro Matiello 2 | # 3 | # Permission is hereby granted, free of charge, to any person 4 | # obtaining a copy of this software and associated documentation 5 | # files (the "Software"), to deal in the Software without 6 | # restriction, including without limitation the rights to use, 7 | # copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | # copies of the Software, and to permit persons to whom the 9 | # Software is furnished to do so, subject to the following 10 | # conditions: 11 | 12 | # The above copyright notice and this permission notice shall be 13 | # included in all copies or substantial portions of the Software. 14 | 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | # OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | 25 | """ 26 | Unittests for graph.algorithms.readwrite 27 | """ 28 | 29 | 30 | import unittest 31 | import pygraph 32 | from pygraph.readwrite import dot, markup 33 | from . import testlib 34 | 35 | def graph_equality(gr1, gr2): 36 | for each in gr1.nodes(): 37 | assert each in gr2.nodes() 38 | for each in gr2.nodes(): 39 | assert each in gr1.nodes() 40 | for each in gr1.edges(): 41 | assert each in gr2.edges() 42 | for each in gr2.edges(): 43 | assert each in gr1.edges() 44 | 45 | class test_readwrite_dot(unittest.TestCase): 46 | 47 | def test_dot_for_graph(self): 48 | gr = testlib.new_graph() 49 | dotstr = dot.write(gr) 50 | gr1 = dot.read(dotstr) 51 | dotstr = dot.write(gr1) 52 | gr2 = dot.read(dotstr) 53 | graph_equality(gr1, gr2) 54 | assert len(gr.nodes()) == len(gr1.nodes()) 55 | assert len(gr.edges()) == len(gr1.edges()) 56 | 57 | def test_dot_for_digraph(self): 58 | gr = testlib.new_digraph() 59 | dotstr = dot.write(gr) 60 | gr1 = dot.read(dotstr) 61 | dotstr = dot.write(gr1) 62 | gr2 = dot.read(dotstr) 63 | graph_equality(gr1, gr2) 64 | assert len(gr.nodes()) == len(gr1.nodes()) 65 | assert len(gr.edges()) == len(gr1.edges()) 66 | 67 | def test_dot_for_hypergraph(self): 68 | gr = testlib.new_hypergraph() 69 | dotstr = dot.write(gr) 70 | gr1 = dot.read_hypergraph(dotstr) 71 | dotstr = dot.write(gr1) 72 | gr2 = dot.read_hypergraph(dotstr) 73 | graph_equality(gr1, gr2) 74 | 75 | def test_output_names_in_dot(self): 76 | gr1 = testlib.new_graph() 77 | gr1.name = "Some name 1" 78 | gr2 = testlib.new_digraph() 79 | gr2.name = "Some name 2" 80 | gr3 = testlib.new_hypergraph() 81 | gr3.name = "Some name 3" 82 | assert "Some name 1" in dot.write(gr1) 83 | assert "Some name 2" in dot.write(gr2) 84 | assert "Some name 3" in dot.write(gr3) 85 | 86 | class test_readwrite_markup(unittest.TestCase): 87 | 88 | def test_xml_for_graph(self): 89 | gr = testlib.new_graph() 90 | dotstr = markup.write(gr) 91 | gr1 = markup.read(dotstr) 92 | dotstr = markup.write(gr1) 93 | gr2 = markup.read(dotstr) 94 | graph_equality(gr1, gr2) 95 | assert len(gr.nodes()) == len(gr1.nodes()) 96 | assert len(gr.edges()) == len(gr1.edges()) 97 | 98 | def test_xml_digraph(self): 99 | gr = testlib.new_digraph() 100 | dotstr = markup.write(gr) 101 | gr1 = markup.read(dotstr) 102 | dotstr = markup.write(gr1) 103 | gr2 = markup.read(dotstr) 104 | graph_equality(gr1, gr2) 105 | assert len(gr.nodes()) == len(gr1.nodes()) 106 | assert len(gr.edges()) == len(gr1.edges()) 107 | 108 | def test_xml_hypergraph(self): 109 | gr = testlib.new_hypergraph() 110 | dotstr = markup.write(gr) 111 | gr1 = markup.read(dotstr) 112 | dotstr = markup.write(gr1) 113 | gr2 = markup.read(dotstr) 114 | graph_equality(gr1, gr2) 115 | 116 | if __name__ == "__main__": 117 | unittest.main() 118 | -------------------------------------------------------------------------------- /tests/unittests-searching.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Pedro Matiello 2 | # 3 | # Permission is hereby granted, free of charge, to any person 4 | # obtaining a copy of this software and associated documentation 5 | # files (the "Software"), to deal in the Software without 6 | # restriction, including without limitation the rights to use, 7 | # copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | # copies of the Software, and to permit persons to whom the 9 | # Software is furnished to do so, subject to the following 10 | # conditions: 11 | 12 | # The above copyright notice and this permission notice shall be 13 | # included in all copies or substantial portions of the Software. 14 | 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | # OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | 25 | """ 26 | Unittests for graph.algorithms.searching 27 | """ 28 | 29 | 30 | # Imports 31 | import unittest 32 | import pygraph 33 | import pygraph.classes 34 | from pygraph.algorithms.searching import depth_first_search, breadth_first_search 35 | from sys import getrecursionlimit 36 | from . import testlib 37 | 38 | 39 | class test_depth_first_search(unittest.TestCase): 40 | 41 | def test_dfs_in_empty_graph(self): 42 | gr = pygraph.classes.graph.graph() 43 | st, pre, post = depth_first_search(gr) 44 | assert st == {} 45 | assert pre == [] 46 | assert post == [] 47 | 48 | def test_dfs_in_graph(self): 49 | gr = testlib.new_graph() 50 | st, pre, post = depth_first_search(gr) 51 | for each in gr: 52 | if (st[each] != None): 53 | assert pre.index(each) > pre.index(st[each]) 54 | assert post.index(each) < post.index(st[each]) 55 | for node in st: 56 | assert gr.has_edge((st[node], node)) or st[node] == None 57 | 58 | def test_dfs_in_empty_digraph(self): 59 | gr = pygraph.classes.digraph.digraph() 60 | st, pre, post = depth_first_search(gr) 61 | assert st == {} 62 | assert pre == [] 63 | assert post == [] 64 | 65 | def test_dfs_in_digraph(self): 66 | gr = testlib.new_digraph() 67 | st, pre, post = depth_first_search(gr) 68 | for each in gr: 69 | if (st[each] != None): 70 | assert pre.index(each) > pre.index(st[each]) 71 | assert post.index(each) < post.index(st[each]) 72 | for node in st: 73 | assert gr.has_edge((st[node], node)) or st[node] == None 74 | 75 | def test_dfs_very_deep_graph(self): 76 | gr = pygraph.classes.graph.graph() 77 | gr.add_nodes(list(range(0,12001))) 78 | for i in range(0,12000): 79 | gr.add_edge((i,i+1)) 80 | recursionlimit = getrecursionlimit() 81 | depth_first_search(gr, 0) 82 | assert getrecursionlimit() == recursionlimit 83 | 84 | class test_breadth_first_search(unittest.TestCase): 85 | 86 | def test_bfs_in_empty_graph(self): 87 | gr = pygraph.classes.graph.graph() 88 | st, lo = breadth_first_search(gr) 89 | assert st == {} 90 | assert lo == [] 91 | 92 | def test_bfs_in_graph(self): 93 | gr = pygraph.classes.graph.graph() 94 | gr = testlib.new_digraph() 95 | st, lo = breadth_first_search(gr) 96 | for each in gr: 97 | if (st[each] != None): 98 | assert lo.index(each) > lo.index(st[each]) 99 | for node in st: 100 | assert gr.has_edge((st[node], node)) or st[node] == None 101 | 102 | def test_bfs_in_empty_digraph(self): 103 | gr = pygraph.classes.digraph.digraph() 104 | st, lo = breadth_first_search(gr) 105 | assert st == {} 106 | assert lo == [] 107 | 108 | def test_bfs_in_digraph(self): 109 | gr = testlib.new_digraph() 110 | st, lo = breadth_first_search(gr) 111 | for each in gr: 112 | if (st[each] != None): 113 | assert lo.index(each) > lo.index(st[each]) 114 | for node in st: 115 | assert gr.has_edge((st[node], node)) or st[node] == None 116 | 117 | if __name__ == "__main__": 118 | unittest.main() 119 | -------------------------------------------------------------------------------- /tests/unittests-sorting.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Pedro Matiello 2 | # 3 | # Permission is hereby granted, free of charge, to any person 4 | # obtaining a copy of this software and associated documentation 5 | # files (the "Software"), to deal in the Software without 6 | # restriction, including without limitation the rights to use, 7 | # copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | # copies of the Software, and to permit persons to whom the 9 | # Software is furnished to do so, subject to the following 10 | # conditions: 11 | 12 | # The above copyright notice and this permission notice shall be 13 | # included in all copies or substantial portions of the Software. 14 | 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | # OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | 25 | """ 26 | Unittests for graph.algorithms.sorting 27 | """ 28 | 29 | 30 | import unittest 31 | import pygraph.classes 32 | from pygraph.algorithms.sorting import topological_sorting 33 | from pygraph.algorithms.searching import depth_first_search 34 | from sys import getrecursionlimit 35 | from . import testlib 36 | 37 | 38 | class test_topological_sorting(unittest.TestCase): 39 | 40 | def test_topological_sorting_on_tree(self): 41 | gr = testlib.new_graph() 42 | st, pre, post = depth_first_search(gr) 43 | tree = pygraph.classes.digraph.digraph() 44 | 45 | 46 | for each in st: 47 | if st[each]: 48 | if (each not in tree.nodes()): 49 | tree.add_node(each) 50 | if (st[each] not in tree.nodes()): 51 | tree.add_node(st[each]) 52 | tree.add_edge((st[each], each)) 53 | 54 | ts = topological_sorting(tree) 55 | for each in ts: 56 | if (st[each]): 57 | assert ts.index(each) > ts.index(st[each]) 58 | 59 | def test_topological_sorting_on_digraph(self): 60 | 61 | def is_ordered(node, list): 62 | # Has parent on list 63 | for each in list: 64 | if gr.has_edge((each, node)): 65 | return True 66 | # Has no possible ancestors on list 67 | st, pre, post = depth_first_search(gr, node) 68 | for each in list: 69 | if (each in st): 70 | return False 71 | return True 72 | 73 | gr = testlib.new_digraph() 74 | ts = topological_sorting(gr) 75 | 76 | while (ts): 77 | x = ts.pop() 78 | assert is_ordered(x, ts) 79 | 80 | def test_topological_sort_on_very_deep_graph(self): 81 | gr = pygraph.classes.graph.graph() 82 | gr.add_nodes(list(range(0,12001))) 83 | for i in range(0,12000): 84 | gr.add_edge((i,i+1)) 85 | recursionlimit = getrecursionlimit() 86 | topological_sorting(gr) 87 | assert getrecursionlimit() == recursionlimit 88 | 89 | 90 | if __name__ == "__main__": 91 | unittest.main() 92 | --------------------------------------------------------------------------------