From ea19192112f46fcbcf4553a7e8b91a344be3055a Mon Sep 17 00:00:00 2001 From: Sebastian Oeltjen Date: Sun, 8 Nov 2015 16:06:43 -0600 Subject: [PATCH] Updated python code to python 3.5 --- core/build/lib/pygraph/__init__.py | 63 +++ core/build/lib/pygraph/algorithms/__init__.py | 31 ++ .../lib/pygraph/algorithms/accessibility.py | 347 ++++++++++++ core/build/lib/pygraph/algorithms/critical.py | 163 ++++++ core/build/lib/pygraph/algorithms/cycles.py | 108 ++++ .../pygraph/algorithms/filters/__init__.py | 29 + .../lib/pygraph/algorithms/filters/find.py | 78 +++ .../lib/pygraph/algorithms/filters/null.py | 68 +++ .../lib/pygraph/algorithms/filters/radius.py | 96 ++++ .../lib/pygraph/algorithms/generators.py | 132 +++++ .../pygraph/algorithms/heuristics/__init__.py | 32 ++ .../lib/pygraph/algorithms/heuristics/chow.py | 77 +++ .../algorithms/heuristics/euclidean.py | 97 ++++ core/build/lib/pygraph/algorithms/minmax.py | 516 ++++++++++++++++++ core/build/lib/pygraph/algorithms/pagerank.py | 76 +++ .../build/lib/pygraph/algorithms/searching.py | 153 ++++++ core/build/lib/pygraph/algorithms/sorting.py | 51 ++ .../build/lib/pygraph/algorithms/traversal.py | 84 +++ core/build/lib/pygraph/algorithms/utils.py | 89 +++ core/build/lib/pygraph/classes/__init__.py | 27 + core/build/lib/pygraph/classes/digraph.py | 259 +++++++++ core/build/lib/pygraph/classes/exceptions.py | 76 +++ core/build/lib/pygraph/classes/graph.py | 230 ++++++++ core/build/lib/pygraph/classes/hypergraph.py | 363 ++++++++++++ core/build/lib/pygraph/mixins/__init__.py | 33 ++ core/build/lib/pygraph/mixins/basegraph.py | 32 ++ core/build/lib/pygraph/mixins/common.py | 215 ++++++++ core/build/lib/pygraph/mixins/labeling.py | 227 ++++++++ core/build/lib/pygraph/readwrite/__init__.py | 31 ++ core/build/lib/pygraph/readwrite/markup.py | 195 +++++++ core/dist/python_graph_core-1.8.2-py3.5.egg | Bin 0 -> 95316 bytes core/distribute_setup.py | 2 +- core/distribute_setup.py.bak | 487 +++++++++++++++++ core/pygraph/algorithms/generators.py | 2 +- core/pygraph/algorithms/generators.py.bak | 132 +++++ core/pygraph/algorithms/minmax.py | 4 +- core/pygraph/algorithms/minmax.py.bak | 516 ++++++++++++++++++ core/pygraph/algorithms/searching.py | 12 +- core/pygraph/algorithms/searching.py.bak | 153 ++++++ core/pygraph/classes/digraph.py | 2 +- core/pygraph/classes/digraph.py.bak | 259 +++++++++ core/pygraph/classes/graph.py | 2 +- core/pygraph/classes/graph.py.bak | 230 ++++++++ core/python_graph_core.egg-info/PKG-INFO | 13 + core/python_graph_core.egg-info/SOURCES.txt | 36 ++ .../dependency_links.txt | 1 + .../namespace_packages.txt | 1 + core/python_graph_core.egg-info/top_level.txt | 5 + dot/build/lib/pygraph/__init__.py | 1 + dot/build/lib/pygraph/readwrite/__init__.py | 1 + dot/build/lib/pygraph/readwrite/dot.py | 263 +++++++++ dot/dist/python_graph_dot-1.8.2-py3.5.egg | Bin 0 -> 8120 bytes dot/distribute_setup.py | 2 +- dot/distribute_setup.py.bak | 487 +++++++++++++++++ dot/pygraph/readwrite/dot.py | 8 +- dot/pygraph/readwrite/dot.py.bak | 263 +++++++++ dot/python_graph_dot.egg-info/PKG-INFO | 13 + dot/python_graph_dot.egg-info/SOURCES.txt | 10 + .../dependency_links.txt | 1 + .../namespace_packages.txt | 1 + dot/python_graph_dot.egg-info/requires.txt | 2 + dot/python_graph_dot.egg-info/top_level.txt | 2 + tests/testlib.py | 4 +- tests/testlib.py.bak | 72 +++ tests/testrunner.py | 6 +- tests/testrunner.py.bak | 76 +++ tests/unittests-accessibility.py | 22 +- tests/unittests-accessibility.py.bak | 327 +++++++++++ tests/unittests-cycles.py | 4 +- tests/unittests-cycles.py.bak | 108 ++++ tests/unittests-heuristics.py | 2 +- tests/unittests-heuristics.py.bak | 94 ++++ tests/unittests-hypergraph.py | 2 +- tests/unittests-hypergraph.py.bak | 339 ++++++++++++ tests/unittests-minmax.py | 2 +- tests/unittests-minmax.py.bak | 230 ++++++++ tests/unittests-pagerank.py | 2 +- tests/unittests-pagerank.py.bak | 103 ++++ tests/unittests-readwrite.py | 2 +- tests/unittests-readwrite.py.bak | 117 ++++ tests/unittests-searching.py | 4 +- tests/unittests-searching.py.bak | 118 ++++ tests/unittests-sorting.py | 4 +- tests/unittests-sorting.py.bak | 90 +++ 84 files changed, 8573 insertions(+), 44 deletions(-) create mode 100644 core/build/lib/pygraph/__init__.py create mode 100644 core/build/lib/pygraph/algorithms/__init__.py create mode 100644 core/build/lib/pygraph/algorithms/accessibility.py create mode 100644 core/build/lib/pygraph/algorithms/critical.py create mode 100644 core/build/lib/pygraph/algorithms/cycles.py create mode 100644 core/build/lib/pygraph/algorithms/filters/__init__.py create mode 100644 core/build/lib/pygraph/algorithms/filters/find.py create mode 100644 core/build/lib/pygraph/algorithms/filters/null.py create mode 100644 core/build/lib/pygraph/algorithms/filters/radius.py create mode 100644 core/build/lib/pygraph/algorithms/generators.py create mode 100644 core/build/lib/pygraph/algorithms/heuristics/__init__.py create mode 100644 core/build/lib/pygraph/algorithms/heuristics/chow.py create mode 100644 core/build/lib/pygraph/algorithms/heuristics/euclidean.py create mode 100644 core/build/lib/pygraph/algorithms/minmax.py create mode 100644 core/build/lib/pygraph/algorithms/pagerank.py create mode 100644 core/build/lib/pygraph/algorithms/searching.py create mode 100644 core/build/lib/pygraph/algorithms/sorting.py create mode 100644 core/build/lib/pygraph/algorithms/traversal.py create mode 100644 core/build/lib/pygraph/algorithms/utils.py create mode 100644 core/build/lib/pygraph/classes/__init__.py create mode 100644 core/build/lib/pygraph/classes/digraph.py create mode 100644 core/build/lib/pygraph/classes/exceptions.py create mode 100644 core/build/lib/pygraph/classes/graph.py create mode 100644 core/build/lib/pygraph/classes/hypergraph.py create mode 100644 core/build/lib/pygraph/mixins/__init__.py create mode 100644 core/build/lib/pygraph/mixins/basegraph.py create mode 100644 core/build/lib/pygraph/mixins/common.py create mode 100644 core/build/lib/pygraph/mixins/labeling.py create mode 100644 core/build/lib/pygraph/readwrite/__init__.py create mode 100644 core/build/lib/pygraph/readwrite/markup.py create mode 100644 core/dist/python_graph_core-1.8.2-py3.5.egg create mode 100644 core/distribute_setup.py.bak create mode 100644 core/pygraph/algorithms/generators.py.bak create mode 100644 core/pygraph/algorithms/minmax.py.bak create mode 100644 core/pygraph/algorithms/searching.py.bak create mode 100644 core/pygraph/classes/digraph.py.bak create mode 100644 core/pygraph/classes/graph.py.bak create mode 100644 core/python_graph_core.egg-info/PKG-INFO create mode 100644 core/python_graph_core.egg-info/SOURCES.txt create mode 100644 core/python_graph_core.egg-info/dependency_links.txt create mode 100644 core/python_graph_core.egg-info/namespace_packages.txt create mode 100644 core/python_graph_core.egg-info/top_level.txt create mode 100644 dot/build/lib/pygraph/__init__.py create mode 100644 dot/build/lib/pygraph/readwrite/__init__.py create mode 100644 dot/build/lib/pygraph/readwrite/dot.py create mode 100644 dot/dist/python_graph_dot-1.8.2-py3.5.egg create mode 100644 dot/distribute_setup.py.bak create mode 100644 dot/pygraph/readwrite/dot.py.bak create mode 100644 dot/python_graph_dot.egg-info/PKG-INFO create mode 100644 dot/python_graph_dot.egg-info/SOURCES.txt create mode 100644 dot/python_graph_dot.egg-info/dependency_links.txt create mode 100644 dot/python_graph_dot.egg-info/namespace_packages.txt create mode 100644 dot/python_graph_dot.egg-info/requires.txt create mode 100644 dot/python_graph_dot.egg-info/top_level.txt create mode 100644 tests/testlib.py.bak create mode 100644 tests/testrunner.py.bak create mode 100644 tests/unittests-accessibility.py.bak create mode 100644 tests/unittests-cycles.py.bak create mode 100644 tests/unittests-heuristics.py.bak create mode 100644 tests/unittests-hypergraph.py.bak create mode 100644 tests/unittests-minmax.py.bak create mode 100644 tests/unittests-pagerank.py.bak create mode 100644 tests/unittests-readwrite.py.bak create mode 100644 tests/unittests-searching.py.bak create mode 100644 tests/unittests-sorting.py.bak diff --git a/core/build/lib/pygraph/__init__.py b/core/build/lib/pygraph/__init__.py new file mode 100644 index 0000000..8c2ee51 --- /dev/null +++ b/core/build/lib/pygraph/__init__.py @@ -0,0 +1,63 @@ +# Copyright (c) 2007-2012 Pedro Matiello +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, +# copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following +# conditions: + +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. + + +""" +B{python-graph} + +A library for working with graphs in Python. + +@version: 1.8.2 + +L{Data structure} classes are located at C{pygraph.classes}. + +L{Exception} classes are located at C{pygraph.classes.exceptions}. + +L{Search filters} are located at C{pygraph.algorithms.filters}. + +L{Heuristics} for the A* algorithm are exposed in +C{pygraph.algorithms.heuristics}. + +A quick introductory example: + +>>> # Import the module and instantiate a graph object +>>> from pygraph.classes.graph import graph +>>> from pygraph.algorithms.searching import depth_first_search +>>> gr = graph() +>>> # Add nodes +>>> gr.add_nodes(['X','Y','Z']) +>>> gr.add_nodes(['A','B','C']) +>>> # Add edges +>>> gr.add_edge(('X','Y')) +>>> gr.add_edge(('X','Z')) +>>> gr.add_edge(('A','B')) +>>> gr.add_edge(('A','C')) +>>> gr.add_edge(('Y','B')) +>>> # Depth first search rooted on node X +>>> st, pre, post = depth_first_search(gr, root='X') +>>> # Print the spanning tree +>>> print st +{'A': 'B', 'C': 'A', 'B': 'Y', 'Y': 'X', 'X': None, 'Z': 'X'} +""" + +__import__('pkg_resources').declare_namespace(__name__) diff --git a/core/build/lib/pygraph/algorithms/__init__.py b/core/build/lib/pygraph/algorithms/__init__.py new file mode 100644 index 0000000..f17ac5e --- /dev/null +++ b/core/build/lib/pygraph/algorithms/__init__.py @@ -0,0 +1,31 @@ +# Copyright (c) 2008-2009 Pedro Matiello +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, +# copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following +# conditions: + +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. + + +""" +Algorithms + +This subpackage contains a set of modules, each one of them containing some algorithms. +""" + +__import__('pkg_resources').declare_namespace(__name__) diff --git a/core/build/lib/pygraph/algorithms/accessibility.py b/core/build/lib/pygraph/algorithms/accessibility.py new file mode 100644 index 0000000..3093a4f --- /dev/null +++ b/core/build/lib/pygraph/algorithms/accessibility.py @@ -0,0 +1,347 @@ +# Copyright (c) 2007-2009 Pedro Matiello +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, +# copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following +# conditions: + +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. + + +""" +Accessibility algorithms. + +@sort: accessibility, connected_components, cut_edges, cut_nodes, mutual_accessibility +""" + + +# Imports +from sys import getrecursionlimit, setrecursionlimit + +# Transitive-closure + +def accessibility(graph): + """ + Accessibility matrix (transitive closure). + + @type graph: graph, digraph, hypergraph + @param graph: Graph. + + @rtype: dictionary + @return: Accessibility information for each node. + """ + recursionlimit = getrecursionlimit() + setrecursionlimit(max(len(graph.nodes())*2,recursionlimit)) + + accessibility = {} # Accessibility matrix + + # For each node i, mark each node j if that exists a path from i to j. + for each in graph: + access = {} + # Perform DFS to explore all reachable nodes + _dfs(graph, access, 1, each) + accessibility[each] = list(access.keys()) + + setrecursionlimit(recursionlimit) + return accessibility + + +# Strongly connected components + +def mutual_accessibility(graph): + """ + Mutual-accessibility matrix (strongly connected components). + + @type graph: graph, digraph + @param graph: Graph. + + @rtype: dictionary + @return: Mutual-accessibility information for each node. + """ + recursionlimit = getrecursionlimit() + setrecursionlimit(max(len(graph.nodes())*2,recursionlimit)) + + mutual_access = {} + stack = [] + low = {} + + def visit(node): + if node in low: + return + + num = len(low) + low[node] = num + stack_pos = len(stack) + stack.append(node) + + for successor in graph.neighbors(node): + visit(successor) + low[node] = min(low[node], low[successor]) + + if num == low[node]: + component = stack[stack_pos:] + del stack[stack_pos:] + component.sort() + for each in component: + mutual_access[each] = component + + for item in component: + low[item] = len(graph) + + for node in graph: + visit(node) + + setrecursionlimit(recursionlimit) + return mutual_access + + +# Connected components + +def connected_components(graph): + """ + Connected components. + + @type graph: graph, hypergraph + @param graph: Graph. + + @rtype: dictionary + @return: Pairing that associates each node to its connected component. + """ + recursionlimit = getrecursionlimit() + setrecursionlimit(max(len(graph.nodes())*2,recursionlimit)) + + visited = {} + count = 1 + + # For 'each' node not found to belong to a connected component, find its connected + # component. + for each in graph: + if (each not in visited): + _dfs(graph, visited, count, each) + count = count + 1 + + setrecursionlimit(recursionlimit) + return visited + + +# Limited DFS implementations used by algorithms here + +def _dfs(graph, visited, count, node): + """ + Depth-first search subfunction adapted for accessibility algorithms. + + @type graph: graph, digraph, hypergraph + @param graph: Graph. + + @type visited: dictionary + @param visited: List of nodes (visited nodes are marked non-zero). + + @type count: number + @param count: Counter of connected components. + + @type node: node + @param node: Node to be explored by DFS. + """ + visited[node] = count + # Explore recursively the connected component + for each in graph[node]: + if (each not in visited): + _dfs(graph, visited, count, each) + + +# Cut-Edge and Cut-Vertex identification + +# This works by creating a spanning tree for the graph and keeping track of the preorder number +# of each node in the graph in pre[]. The low[] number for each node tracks the pre[] number of +# the node with lowest pre[] number reachable from the first node. +# +# An edge (u, v) will be a cut-edge low[u] == pre[v]. Suppose v under the spanning subtree with +# root u. This means that, from u, through a path inside this subtree, followed by an backarc, +# one can not get out the subtree. So, (u, v) is the only connection between this subtree and +# the remaining parts of the graph and, when removed, will increase the number of connected +# components. + +# Similarly, a node u will be a cut node if any of the nodes v in the spanning subtree rooted in +# u are so that low[v] > pre[u], which means that there's no path from v to outside this subtree +# without passing through u. + +def cut_edges(graph): + """ + Return the cut-edges of the given graph. + + A cut edge, or bridge, is an edge of a graph whose removal increases the number of connected + components in the graph. + + @type graph: graph, hypergraph + @param graph: Graph. + + @rtype: list + @return: List of cut-edges. + """ + recursionlimit = getrecursionlimit() + setrecursionlimit(max(len(graph.nodes())*2,recursionlimit)) + + # Dispatch if we have a hypergraph + if 'hypergraph' == graph.__class__.__name__: + return _cut_hyperedges(graph) + + pre = {} # Pre-ordering + low = {} # Lowest pre[] reachable from this node going down the spanning tree + one backedge + spanning_tree = {} + reply = [] + pre[None] = 0 + + for each in graph: + if (each not in pre): + spanning_tree[each] = None + _cut_dfs(graph, spanning_tree, pre, low, reply, each) + + setrecursionlimit(recursionlimit) + return reply + + +def _cut_hyperedges(hypergraph): + """ + Return the cut-hyperedges of the given hypergraph. + + @type hypergraph: hypergraph + @param hypergraph: Hypergraph + + @rtype: list + @return: List of cut-nodes. + """ + edges_ = cut_nodes(hypergraph.graph) + edges = [] + + for each in edges_: + if (each[1] == 'h'): + edges.append(each[0]) + + return edges + + +def cut_nodes(graph): + """ + Return the cut-nodes of the given graph. + + A cut node, or articulation point, is a node of a graph whose removal increases the number of + connected components in the graph. + + @type graph: graph, hypergraph + @param graph: Graph. + + @rtype: list + @return: List of cut-nodes. + """ + recursionlimit = getrecursionlimit() + setrecursionlimit(max(len(graph.nodes())*2,recursionlimit)) + + # Dispatch if we have a hypergraph + if 'hypergraph' == graph.__class__.__name__: + return _cut_hypernodes(graph) + + pre = {} # Pre-ordering + low = {} # Lowest pre[] reachable from this node going down the spanning tree + one backedge + reply = {} + spanning_tree = {} + pre[None] = 0 + + # Create spanning trees, calculate pre[], low[] + for each in graph: + if (each not in pre): + spanning_tree[each] = None + _cut_dfs(graph, spanning_tree, pre, low, [], each) + + # Find cuts + for each in graph: + # If node is not a root + if (spanning_tree[each] is not None): + for other in graph[each]: + # If there is no back-edge from descendent to a ancestral of each + if (low[other] >= pre[each] and spanning_tree[other] == each): + reply[each] = 1 + # If node is a root + else: + children = 0 + for other in graph: + if (spanning_tree[other] == each): + children = children + 1 + # root is cut-vertex iff it has two or more children + if (children >= 2): + reply[each] = 1 + + setrecursionlimit(recursionlimit) + return list(reply.keys()) + + +def _cut_hypernodes(hypergraph): + """ + Return the cut-nodes of the given hypergraph. + + @type hypergraph: hypergraph + @param hypergraph: Hypergraph + + @rtype: list + @return: List of cut-nodes. + """ + nodes_ = cut_nodes(hypergraph.graph) + nodes = [] + + for each in nodes_: + if (each[1] == 'n'): + nodes.append(each[0]) + + return nodes + + +def _cut_dfs(graph, spanning_tree, pre, low, reply, node): + """ + Depth first search adapted for identification of cut-edges and cut-nodes. + + @type graph: graph, digraph + @param graph: Graph + + @type spanning_tree: dictionary + @param spanning_tree: Spanning tree being built for the graph by DFS. + + @type pre: dictionary + @param pre: Graph's preordering. + + @type low: dictionary + @param low: Associates to each node, the preordering index of the node of lowest preordering + accessible from the given node. + + @type reply: list + @param reply: List of cut-edges. + + @type node: node + @param node: Node to be explored by DFS. + """ + pre[node] = pre[None] + low[node] = pre[None] + pre[None] = pre[None] + 1 + + for each in graph[node]: + if (each not in pre): + spanning_tree[each] = node + _cut_dfs(graph, spanning_tree, pre, low, reply, each) + if (low[node] > low[each]): + low[node] = low[each] + if (low[each] == pre[each]): + reply.append((node, each)) + elif (low[node] > pre[each] and spanning_tree[node] != each): + low[node] = pre[each] diff --git a/core/build/lib/pygraph/algorithms/critical.py b/core/build/lib/pygraph/algorithms/critical.py new file mode 100644 index 0000000..2e359e6 --- /dev/null +++ b/core/build/lib/pygraph/algorithms/critical.py @@ -0,0 +1,163 @@ +# Copyright (c) 2009 Pedro Matiello +# Tomaz Kovacic +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, +# copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following +# conditions: + +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. + + +""" +Critical path algorithms and transitivity detection algorithm. + +@sort: critical_path, transitive_edges +""" + + +# Imports +from pygraph.algorithms.cycles import find_cycle +from pygraph.algorithms.traversal import traversal +from pygraph.algorithms.sorting import topological_sorting + +def _intersection(A,B): + """ + A simple function to find an intersection between two arrays. + + @type A: List + @param A: First List + + @type B: List + @param B: Second List + + @rtype: List + @return: List of Intersections + """ + intersection = [] + for i in A: + if i in B: + intersection.append(i) + return intersection + +def transitive_edges(graph): + """ + Return a list of transitive edges. + + Example of transitivity within graphs: A -> B, B -> C, A -> C + in this case the transitive edge is: A -> C + + @attention: This function is only meaningful for directed acyclic graphs. + + @type graph: digraph + @param graph: Digraph + + @rtype: List + @return: List containing tuples with transitive edges (or an empty array if the digraph + contains a cycle) + """ + #if the graph contains a cycle we return an empty array + if not len(find_cycle(graph)) == 0: + return [] + + tranz_edges = [] # create an empty array that will contain all the tuples + + #run trough all the nodes in the graph + for start in topological_sorting(graph): + #find all the successors on the path for the current node + successors = [] + for a in traversal(graph,start,'pre'): + successors.append(a) + del successors[0] #we need all the nodes in it's path except the start node itself + + for next in successors: + #look for an intersection between all the neighbors of the + #given node and all the neighbors from the given successor + intersect_array = _intersection(graph.neighbors(next), graph.neighbors(start) ) + for a in intersect_array: + if graph.has_edge((start, a)): + ##check for the detected edge and append it to the returned array + tranz_edges.append( (start,a) ) + return tranz_edges # return the final array + + +def critical_path(graph): + """ + Compute and return the critical path in an acyclic directed weighted graph. + + @attention: This function is only meaningful for directed weighted acyclic graphs + + @type graph: digraph + @param graph: Digraph + + @rtype: List + @return: List containing all the nodes in the path (or an empty array if the graph + contains a cycle) + """ + #if the graph contains a cycle we return an empty array + if not len(find_cycle(graph)) == 0: + return [] + + #this empty dictionary will contain a tuple for every single node + #the tuple contains the information about the most costly predecessor + #of the given node and the cost of the path to this node + #(predecessor, cost) + node_tuples = {} + + topological_nodes = topological_sorting(graph) + + #all the tuples must be set to a default value for every node in the graph + for node in topological_nodes: + node_tuples.update( {node :(None, 0)} ) + + #run trough all the nodes in a topological order + for node in topological_nodes: + predecessors =[] + #we must check all the predecessors + for pre in graph.incidents(node): + max_pre = node_tuples[pre][1] + predecessors.append( (pre, graph.edge_weight( (pre, node) ) + max_pre ) ) + + max = 0; max_tuple = (None, 0) + for i in predecessors:#look for the most costly predecessor + if i[1] >= max: + max = i[1] + max_tuple = i + #assign the maximum value to the given node in the node_tuples dictionary + node_tuples[node] = max_tuple + + #find the critical node + max = 0; critical_node = None + for k,v in list(node_tuples.items()): + if v[1] >= max: + max= v[1] + critical_node = k + + + path = [] + #find the critical path with backtracking trought the dictionary + def mid_critical_path(end): + if node_tuples[end][0] != None: + path.append(end) + mid_critical_path(node_tuples[end][0]) + else: + path.append(end) + #call the recursive function + mid_critical_path(critical_node) + + path.reverse() + return path #return the array containing the critical path \ No newline at end of file diff --git a/core/build/lib/pygraph/algorithms/cycles.py b/core/build/lib/pygraph/algorithms/cycles.py new file mode 100644 index 0000000..d2134ef --- /dev/null +++ b/core/build/lib/pygraph/algorithms/cycles.py @@ -0,0 +1,108 @@ +# Copyright (c) 2008-2009 Pedro Matiello +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, +# copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following +# conditions: + +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. + + +""" +Cycle detection algorithms. + +@sort: find_cycle +""" + + +# Imports +from pygraph.classes.exceptions import InvalidGraphType +from pygraph.classes.digraph import digraph as digraph_class +from pygraph.classes.graph import graph as graph_class +from sys import getrecursionlimit, setrecursionlimit + +def find_cycle(graph): + """ + Find a cycle in the given graph. + + This function will return a list of nodes which form a cycle in the graph or an empty list if + no cycle exists. + + @type graph: graph, digraph + @param graph: Graph. + + @rtype: list + @return: List of nodes. + """ + + if (isinstance(graph, graph_class)): + directed = False + elif (isinstance(graph, digraph_class)): + directed = True + else: + raise InvalidGraphType + + def find_cycle_to_ancestor(node, ancestor): + """ + Find a cycle containing both node and ancestor. + """ + path = [] + while (node != ancestor): + if (node is None): + return [] + path.append(node) + node = spanning_tree[node] + path.append(node) + path.reverse() + return path + + def dfs(node): + """ + Depth-first search subfunction. + """ + visited[node] = 1 + # Explore recursively the connected component + for each in graph[node]: + if (cycle): + return + if (each not in visited): + spanning_tree[each] = node + dfs(each) + else: + if (directed or spanning_tree[node] != each): + cycle.extend(find_cycle_to_ancestor(node, each)) + + recursionlimit = getrecursionlimit() + setrecursionlimit(max(len(graph.nodes())*2,recursionlimit)) + + visited = {} # List for marking visited and non-visited nodes + spanning_tree = {} # Spanning tree + cycle = [] + + # Algorithm outer-loop + for each in graph: + # Select a non-visited node + if (each not in visited): + spanning_tree[each] = None + # Explore node's connected component + dfs(each) + if (cycle): + setrecursionlimit(recursionlimit) + return cycle + + setrecursionlimit(recursionlimit) + return [] diff --git a/core/build/lib/pygraph/algorithms/filters/__init__.py b/core/build/lib/pygraph/algorithms/filters/__init__.py new file mode 100644 index 0000000..ad7986e --- /dev/null +++ b/core/build/lib/pygraph/algorithms/filters/__init__.py @@ -0,0 +1,29 @@ +# Copyright (c) 2008-2009 Pedro Matiello +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, +# copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following +# conditions: + +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. + + +""" +Set of searching filters. +""" + +__import__('pkg_resources').declare_namespace(__name__) diff --git a/core/build/lib/pygraph/algorithms/filters/find.py b/core/build/lib/pygraph/algorithms/filters/find.py new file mode 100644 index 0000000..b586d91 --- /dev/null +++ b/core/build/lib/pygraph/algorithms/filters/find.py @@ -0,0 +1,78 @@ +# Copyright (c) 2008-2009 Pedro Matiello +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, +# copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following +# conditions: + +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. + + +""" +Search filter for finding a specific node. +""" + + +class find(object): + """ + Search filter for finding a specific node. + """ + + def __init__(self, target): + """ + Initialize the filter. + + @type target: node + @param target: Target node. + """ + self.graph = None + self.spanning_tree = None + self.target = target + self.done = False + + def configure(self, graph, spanning_tree): + """ + Configure the filter. + + @type graph: graph + @param graph: Graph. + + @type spanning_tree: dictionary + @param spanning_tree: Spanning tree. + """ + self.graph = graph + self.spanning_tree = spanning_tree + + def __call__(self, node, parent): + """ + Decide if the given node should be included in the search process. + + @type node: node + @param node: Given node. + + @type parent: node + @param parent: Given node's parent in the spanning tree. + + @rtype: boolean + @return: Whether the given node should be included in the search process. + """ + if (not self.done): + if (node == self.target): + self.done = True + return True + else: + return False \ No newline at end of file diff --git a/core/build/lib/pygraph/algorithms/filters/null.py b/core/build/lib/pygraph/algorithms/filters/null.py new file mode 100644 index 0000000..6a09a63 --- /dev/null +++ b/core/build/lib/pygraph/algorithms/filters/null.py @@ -0,0 +1,68 @@ +# Copyright (c) 2008-2009 Pedro Matiello +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, +# copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following +# conditions: + +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. + + +""" +Null searching filter. +""" + + +class null(object): + """ + Null search filter. + """ + + def __init__(self): + """ + Initialize the filter. + """ + self.graph = None + self.spanning_tree = None + + def configure(self, graph, spanning_tree): + """ + Configure the filter. + + @type graph: graph + @param graph: Graph. + + @type spanning_tree: dictionary + @param spanning_tree: Spanning tree. + """ + self.graph = graph + self.spanning_tree = spanning_tree + + def __call__(self, node, parent): + """ + Decide if the given node should be included in the search process. + + @type node: node + @param node: Given node. + + @type parent: node + @param parent: Given node's parent in the spanning tree. + + @rtype: boolean + @return: Whether the given node should be included in the search process. + """ + return True \ No newline at end of file diff --git a/core/build/lib/pygraph/algorithms/filters/radius.py b/core/build/lib/pygraph/algorithms/filters/radius.py new file mode 100644 index 0000000..6c9d567 --- /dev/null +++ b/core/build/lib/pygraph/algorithms/filters/radius.py @@ -0,0 +1,96 @@ +# Copyright (c) 2008-2009 Pedro Matiello +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, +# copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following +# conditions: + +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. + + +""" +Radial search filter. +""" + + +class radius(object): + """ + Radial search filter. + + This will keep searching contained inside a specified limit. + """ + + def __init__(self, radius): + """ + Initialize the filter. + + @type radius: number + @param radius: Search radius. + """ + self.graph = None + self.spanning_tree = None + self.radius = radius + self.done = False + + def configure(self, graph, spanning_tree): + """ + Configure the filter. + + @type graph: graph + @param graph: Graph. + + @type spanning_tree: dictionary + @param spanning_tree: Spanning tree. + """ + self.graph = graph + self.spanning_tree = spanning_tree + + def __call__(self, node, parent): + """ + Decide if the given node should be included in the search process. + + @type node: node + @param node: Given node. + + @type parent: node + @param parent: Given node's parent in the spanning tree. + + @rtype: boolean + @return: Whether the given node should be included in the search process. + """ + + def cost_to_root(node): + if (node is not None): + return cost_to_parent(node, st[node]) + cost_to_root(st[node]) + else: + return 0 + + def cost_to_parent(node, parent): + if (parent is not None): + return gr.edge_weight((parent, node)) + else: + return 0 + + gr = self.graph + st = self.spanning_tree + + cost = cost_to_parent(node, parent) + cost_to_root(parent) + + if (cost <= self.radius): + return True + else: + return False \ No newline at end of file diff --git a/core/build/lib/pygraph/algorithms/generators.py b/core/build/lib/pygraph/algorithms/generators.py new file mode 100644 index 0000000..04f92e9 --- /dev/null +++ b/core/build/lib/pygraph/algorithms/generators.py @@ -0,0 +1,132 @@ +# Copyright (c) 2008-2009 Pedro Matiello +# Zsolt Haraszti +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, +# copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following +# conditions: + +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. + + +""" +Random graph generators. + +@sort: generate, generate_hypergraph +""" + + +# Imports +from pygraph.classes.graph import graph +from pygraph.classes.digraph import digraph +from pygraph.classes.hypergraph import hypergraph +from random import randint, choice, shuffle #@UnusedImport +from time import time + +# Generator + +def generate(num_nodes, num_edges, directed=False, weight_range=(1, 1)): + """ + Create a random graph. + + @type num_nodes: number + @param num_nodes: Number of nodes. + + @type num_edges: number + @param num_edges: Number of edges. + + @type directed: bool + @param directed: Whether the generated graph should be directed or not. + + @type weight_range: tuple + @param weight_range: tuple of two integers as lower and upper limits on randomly generated + weights (uniform distribution). + """ + # Graph creation + if directed: + random_graph = digraph() + else: + random_graph = graph() + + # Nodes + nodes = list(range(num_nodes)) + random_graph.add_nodes(nodes) + + # Build a list of all possible edges + edges = [] + edges_append = edges.append + for x in nodes: + for y in nodes: + if ((directed and x != y) or (x > y)): + edges_append((x, y)) + + # Randomize the list + shuffle(edges) + + # Add edges to the graph + min_wt = min(weight_range) + max_wt = max(weight_range) + for i in range(num_edges): + each = edges[i] + random_graph.add_edge((each[0], each[1]), wt = randint(min_wt, max_wt)) + + return random_graph + + +def generate_hypergraph(num_nodes, num_edges, r = 0): + """ + Create a random hyper graph. + + @type num_nodes: number + @param num_nodes: Number of nodes. + + @type num_edges: number + @param num_edges: Number of edges. + + @type r: number + @param r: Uniform edges of size r. + """ + # Graph creation + random_graph = hypergraph() + + # Nodes + nodes = list(map(str, list(range(num_nodes)))) + random_graph.add_nodes(nodes) + + # Base edges + edges = list(map(str, list(range(num_nodes, num_nodes+num_edges)))) + random_graph.add_hyperedges(edges) + + # Connect the edges + if 0 == r: + # Add each edge with 50/50 probability + for e in edges: + for n in nodes: + if choice([True, False]): + random_graph.link(n, e) + + else: + # Add only uniform edges + for e in edges: + # First shuffle the nodes + shuffle(nodes) + + # Then take the first r nodes + for i in range(r): + random_graph.link(nodes[i], e) + + return random_graph diff --git a/core/build/lib/pygraph/algorithms/heuristics/__init__.py b/core/build/lib/pygraph/algorithms/heuristics/__init__.py new file mode 100644 index 0000000..2f7ca4d --- /dev/null +++ b/core/build/lib/pygraph/algorithms/heuristics/__init__.py @@ -0,0 +1,32 @@ +# Copyright (c) 2008-2009 Pedro Matiello +# Salim Fadhley +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, +# copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following +# conditions: + +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. + + +""" +Set of search heuristics. + +These are to be used with the C{heuristic_search()} function. +""" + +__import__('pkg_resources').declare_namespace(__name__) diff --git a/core/build/lib/pygraph/algorithms/heuristics/chow.py b/core/build/lib/pygraph/algorithms/heuristics/chow.py new file mode 100644 index 0000000..bfd7e14 --- /dev/null +++ b/core/build/lib/pygraph/algorithms/heuristics/chow.py @@ -0,0 +1,77 @@ +# Copyright (c) 2008-2009 Pedro Matiello +# Salim Fadhley +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, +# copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following +# conditions: + +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. + + +""" +Edmond Chow's heuristic for A*. +""" + + +# Imports +from pygraph.algorithms.minmax import shortest_path + + +class chow(object): + """ + An implementation of the graph searching heuristic proposed by Edmond Chow. + + Remember to call the C{optimize()} method before the heuristic search. + + For details, check: U{http://www.edmondchow.com/pubs/levdiff-aaai.pdf}. + """ + + def __init__(self, *centers): + """ + Initialize a Chow heuristic object. + """ + self.centers = centers + self.nodes = {} + + def optimize(self, graph): + """ + Build a dictionary mapping each pair of nodes to a number (the distance between them). + + @type graph: graph + @param graph: Graph. + """ + for center in self.centers: + shortest_routes = shortest_path(graph, center)[1] + for node, weight in list(shortest_routes.items()): + self.nodes.setdefault(node, []).append(weight) + + def __call__(self, start, end): + """ + Estimate how far start is from end. + + @type start: node + @param start: Start node. + + @type end: node + @param end: End node. + """ + assert len( list(self.nodes.keys()) ) > 0, "You need to optimize this heuristic for your graph before it can be used to estimate." + + cmp_sequence = list(zip( self.nodes[start], self.nodes[end] )) + chow_number = max( abs( a-b ) for a,b in cmp_sequence ) + return chow_number diff --git a/core/build/lib/pygraph/algorithms/heuristics/euclidean.py b/core/build/lib/pygraph/algorithms/heuristics/euclidean.py new file mode 100644 index 0000000..f0cbc1a --- /dev/null +++ b/core/build/lib/pygraph/algorithms/heuristics/euclidean.py @@ -0,0 +1,97 @@ +# Copyright (c) 2008-2009 Pedro Matiello +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, +# copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following +# conditions: + +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. + + +""" +A* heuristic for euclidean graphs. +""" + + +# Imports + + +class euclidean(object): + """ + A* heuristic for Euclidean graphs. + + This heuristic has three requirements: + 1. All nodes should have the attribute 'position'; + 2. The weight of all edges should be the euclidean distance between the nodes it links; + 3. The C{optimize()} method should be called before the heuristic search. + + A small example for clarification: + + >>> g = graph.graph() + >>> g.add_nodes(['A','B','C']) + >>> g.add_node_attribute('A', ('position',(0,0))) + >>> g.add_node_attribute('B', ('position',(1,1))) + >>> g.add_node_attribute('C', ('position',(0,2))) + >>> g.add_edge('A','B', wt=2) + >>> g.add_edge('B','C', wt=2) + >>> g.add_edge('A','C', wt=4) + >>> h = graph.heuristics.euclidean() + >>> h.optimize(g) + >>> g.heuristic_search('A', 'C', h) + """ + + def __init__(self): + """ + Initialize the heuristic object. + """ + self.distances = {} + + def optimize(self, graph): + """ + Build a dictionary mapping each pair of nodes to a number (the distance between them). + + @type graph: graph + @param graph: Graph. + """ + for start in graph.nodes(): + for end in graph.nodes(): + for each in graph.node_attributes(start): + if (each[0] == 'position'): + start_attr = each[1] + break + for each in graph.node_attributes(end): + if (each[0] == 'position'): + end_attr = each[1] + break + dist = 0 + for i in range(len(start_attr)): + dist = dist + (float(start_attr[i]) - float(end_attr[i]))**2 + self.distances[(start,end)] = dist + + def __call__(self, start, end): + """ + Estimate how far start is from end. + + @type start: node + @param start: Start node. + + @type end: node + @param end: End node. + """ + assert len(list(self.distances.keys())) > 0, "You need to optimize this heuristic for your graph before it can be used to estimate." + + return self.distances[(start,end)] \ No newline at end of file diff --git a/core/build/lib/pygraph/algorithms/minmax.py b/core/build/lib/pygraph/algorithms/minmax.py new file mode 100644 index 0000000..95d4baa --- /dev/null +++ b/core/build/lib/pygraph/algorithms/minmax.py @@ -0,0 +1,516 @@ +# Copyright (c) 2007-2009 Pedro Matiello +# Peter Sagerson +# Johannes Reinhardt +# Rhys Ulerich +# Roy Smith +# Salim Fadhley +# Tomaz Kovacic +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, +# copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following +# conditions: + +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. + + +""" +Minimization and maximization algorithms. + +@sort: heuristic_search, minimal_spanning_tree, shortest_path, +shortest_path_bellman_ford +""" + +from pygraph.algorithms.utils import heappush, heappop +from pygraph.classes.exceptions import NodeUnreachable +from pygraph.classes.exceptions import NegativeWeightCycleError +from pygraph.classes.digraph import digraph +import bisect + +# Minimal spanning tree + +def minimal_spanning_tree(graph, root=None): + """ + Minimal spanning tree. + + @attention: Minimal spanning tree is meaningful only for weighted graphs. + + @type graph: graph + @param graph: Graph. + + @type root: node + @param root: Optional root node (will explore only root's connected component) + + @rtype: dictionary + @return: Generated spanning tree. + """ + visited = [] # List for marking visited and non-visited nodes + spanning_tree = {} # MInimal Spanning tree + + # Initialization + if (root is not None): + visited.append(root) + nroot = root + spanning_tree[root] = None + else: + nroot = 1 + + # Algorithm loop + while (nroot is not None): + ledge = _lightest_edge(graph, visited) + if (ledge == None): + if (root is not None): + break + nroot = _first_unvisited(graph, visited) + if (nroot is not None): + spanning_tree[nroot] = None + visited.append(nroot) + else: + spanning_tree[ledge[1]] = ledge[0] + visited.append(ledge[1]) + + return spanning_tree + + +def _first_unvisited(graph, visited): + """ + Return first unvisited node. + + @type graph: graph + @param graph: Graph. + + @type visited: list + @param visited: List of nodes. + + @rtype: node + @return: First unvisited node. + """ + for each in graph: + if (each not in visited): + return each + return None + + +def _lightest_edge(graph, visited): + """ + Return the lightest edge in graph going from a visited node to an unvisited one. + + @type graph: graph + @param graph: Graph. + + @type visited: list + @param visited: List of nodes. + + @rtype: tuple + @return: Lightest edge in graph going from a visited node to an unvisited one. + """ + lightest_edge = None + weight = None + for each in visited: + for other in graph[each]: + if (other not in visited): + w = graph.edge_weight((each, other)) + if (weight is None or w < weight or weight < 0): + lightest_edge = (each, other) + weight = w + return lightest_edge + + +# Shortest Path + +def shortest_path(graph, source): + """ + Return the shortest path distance between source and all other nodes using Dijkstra's + algorithm. + + @attention: All weights must be nonnegative. + + @see: shortest_path_bellman_ford + + @type graph: graph, digraph + @param graph: Graph. + + @type source: node + @param source: Node from which to start the search. + + @rtype: tuple + @return: A tuple containing two dictionaries, each keyed by target nodes. + 1. Shortest path spanning tree + 2. Shortest distance from given source to each target node + Inaccessible target nodes do not appear in either dictionary. + """ + # Initialization + dist = {source: 0} + previous = {source: None} + + # This is a sorted queue of (dist, node) 2-tuples. The first item in the + # queue is always either a finalized node that we can ignore or the node + # with the smallest estimated distance from the source. Note that we will + # not remove nodes from this list as they are finalized; we just ignore them + # when they come up. + q = [(0, source)] + + # The set of nodes for which we have final distances. + finished = set() + + # Algorithm loop + while len(q) > 0: + du, u = q.pop(0) + + # Process reachable, remaining nodes from u + if u not in finished: + finished.add(u) + for v in graph[u]: + if v not in finished: + alt = du + graph.edge_weight((u, v)) + if (v not in dist) or (alt < dist[v]): + dist[v] = alt + previous[v] = u + bisect.insort(q, (alt, v)) + + return previous, dist + + + +def shortest_path_bellman_ford(graph, source): + """ + Return the shortest path distance between the source node and all other + nodes in the graph using Bellman-Ford's algorithm. + + This algorithm is useful when you have a weighted (and obviously + a directed) graph with negative weights. + + @attention: The algorithm can detect negative weight cycles and will raise + an exception. It's meaningful only for directed weighted graphs. + + @see: shortest_path + + @type graph: digraph + @param graph: Digraph + + @type source: node + @param source: Source node of the graph + + @raise NegativeWeightCycleError: raises if it finds a negative weight cycle. + If this condition is met d(v) > d(u) + W(u, v) then raise the error. + + @rtype: tuple + @return: A tuple containing two dictionaries, each keyed by target nodes. + (same as shortest_path function that implements Dijkstra's algorithm) + 1. Shortest path spanning tree + 2. Shortest distance from given source to each target node + """ + # initialize the required data structures + distance = {source : 0} + predecessor = {source : None} + + # iterate and relax edges + for i in range(1,graph.order()-1): + for src,dst in graph.edges(): + if (src in distance) and (dst not in distance): + distance[dst] = distance[src] + graph.edge_weight((src,dst)) + predecessor[dst] = src + elif (src in distance) and (dst in distance) and \ + distance[src] + graph.edge_weight((src,dst)) < distance[dst]: + distance[dst] = distance[src] + graph.edge_weight((src,dst)) + predecessor[dst] = src + + # detect negative weight cycles + for src,dst in graph.edges(): + if src in distance and \ + dst in distance and \ + distance[src] + graph.edge_weight((src,dst)) < distance[dst]: + raise NegativeWeightCycleError("Detected a negative weight cycle on edge (%s, %s)" % (src,dst)) + + return predecessor, distance + +#Heuristics search + +def heuristic_search(graph, start, goal, heuristic): + """ + A* search algorithm. + + A set of heuristics is available under C{graph.algorithms.heuristics}. User-created heuristics + are allowed too. + + @type graph: graph, digraph + @param graph: Graph + + @type start: node + @param start: Start node + + @type goal: node + @param goal: Goal node + + @type heuristic: function + @param heuristic: Heuristic function + + @rtype: list + @return: Optimized path from start to goal node + """ + + # The queue stores priority, node, cost to reach, and parent. + queue = [ (0, start, 0, None) ] + + # This dictionary maps queued nodes to distance of discovered paths + # and the computed heuristics to goal. We avoid to compute the heuristics + # more than once and to insert too many times the node in the queue. + g = {} + + # This maps explored nodes to parent closest to the start + explored = {} + + while queue: + _, current, dist, parent = heappop(queue) + + if current == goal: + path = [current] + [ n for n in _reconstruct_path( parent, explored ) ] + path.reverse() + return path + + if current in explored: + continue + + explored[current] = parent + + for neighbor in graph[current]: + if neighbor in explored: + continue + + ncost = dist + graph.edge_weight((current, neighbor)) + + if neighbor in g: + qcost, h = g[neighbor] + if qcost <= ncost: + continue + # if ncost < qcost, a longer path to neighbor remains + # g. Removing it would need to filter the whole + # queue, it's better just to leave it there and ignore + # it when we visit the node a second time. + else: + h = heuristic(neighbor, goal) + + g[neighbor] = ncost, h + heappush(queue, (ncost + h, neighbor, ncost, current)) + + raise NodeUnreachable( start, goal ) + +def _reconstruct_path(node, parents): + while node is not None: + yield node + node = parents[node] + +#maximum flow/minimum cut + +def maximum_flow(graph, source, sink, caps = None): + """ + Find a maximum flow and minimum cut of a directed graph by the Edmonds-Karp algorithm. + + @type graph: digraph + @param graph: Graph + + @type source: node + @param source: Source of the flow + + @type sink: node + @param sink: Sink of the flow + + @type caps: dictionary + @param caps: Dictionary specifying a maximum capacity for each edge. If not given, the weight of the edge + will be used as its capacity. Otherwise, for each edge (a,b), caps[(a,b)] should be given. + + @rtype: tuple + @return: A tuple containing two dictionaries + 1. contains the flow through each edge for a maximal flow through the graph + 2. contains to which component of a minimum cut each node belongs + """ + + #handle optional argument, if weights are available, use them, if not, assume one + if caps == None: + caps = {} + for edge in graph.edges(): + caps[edge] = graph.edge_weight((edge[0],edge[1])) + + #data structures to maintain + f = {}.fromkeys(graph.edges(),0) + label = {}.fromkeys(graph.nodes(),[]) + label[source] = ['-',float('Inf')] + u = {}.fromkeys(graph.nodes(),False) + d = {}.fromkeys(graph.nodes(),float('Inf')) + #queue for labelling + q = [source] + + finished = False + while not finished: + #choose first labelled vertex with u == false + for i in range(len(q)): + if not u[q[i]]: + v = q.pop(i) + break + + #find augmenting path + for w in graph.neighbors(v): + if label[w] == [] and f[(v,w)] < caps[(v,w)]: + d[w] = min(caps[(v,w)] - f[(v,w)],d[v]) + label[w] = [v,'+',d[w]] + q.append(w) + for w in graph.incidents(v): + if label[w] == [] and f[(w,v)] > 0: + d[w] = min(f[(w,v)],d[v]) + label[w] = [v,'-',d[w]] + q.append(w) + + u[v] = True + + #extend flow by augmenting path + if label[sink] != []: + delta = label[sink][-1] + w = sink + while w != source: + v = label[w][0] + if label[w][1] == '-': + f[(w,v)] = f[(w,v)] - delta + else: + f[(v,w)] = f[(v,w)] + delta + w = v + #reset labels + label = {}.fromkeys(graph.nodes(),[]) + label[source] = ['-',float('Inf')] + q = [source] + u = {}.fromkeys(graph.nodes(),False) + d = {}.fromkeys(graph.nodes(),float('Inf')) + + #check whether finished + finished = True + for node in graph.nodes(): + if label[node] != [] and u[node] == False: + finished = False + + #find the two components of the cut + cut = {} + for node in graph.nodes(): + if label[node] == []: + cut[node] = 1 + else: + cut[node] = 0 + return (f,cut) + +def cut_value(graph, flow, cut): + """ + Calculate the value of a cut. + + @type graph: digraph + @param graph: Graph + + @type flow: dictionary + @param flow: Dictionary containing a flow for each edge. + + @type cut: dictionary + @param cut: Dictionary mapping each node to a subset index. The function only considers the flow between + nodes with 0 and 1. + + @rtype: float + @return: The value of the flow between the subsets 0 and 1 + """ + #max flow/min cut value calculation + S = [] + T = [] + for node in list(cut.keys()): + if cut[node] == 0: + S.append(node) + elif cut[node] == 1: + T.append(node) + value = 0 + for node in S: + for neigh in graph.neighbors(node): + if neigh in T: + value = value + flow[(node,neigh)] + for inc in graph.incidents(node): + if inc in T: + value = value - flow[(inc,node)] + return value + +def cut_tree(igraph, caps = None): + """ + Construct a Gomory-Hu cut tree by applying the algorithm of Gusfield. + + @type igraph: graph + @param igraph: Graph + + @type caps: dictionary + @param caps: Dictionary specifying a maximum capacity for each edge. If not given, the weight of the edge + will be used as its capacity. Otherwise, for each edge (a,b), caps[(a,b)] should be given. + + @rtype: dictionary + @return: Gomory-Hu cut tree as a dictionary, where each edge is associated with its weight + """ + + #maximum flow needs a digraph, we get a graph + #I think this conversion relies on implementation details outside the api and may break in the future + graph = digraph() + graph.add_graph(igraph) + + #handle optional argument + if not caps: + caps = {} + for edge in graph.edges(): + caps[edge] = igraph.edge_weight(edge) + + #temporary flow variable + f = {} + + #we use a numbering of the nodes for easier handling + n = {} + N = 0 + for node in graph.nodes(): + n[N] = node + N = N + 1 + + #predecessor function + p = {}.fromkeys(list(range(N)),0) + p[0] = None + + for s in range(1,N): + t = p[s] + S = [] + #max flow calculation + (flow,cut) = maximum_flow(graph,n[s],n[t],caps) + for i in range(N): + if cut[n[i]] == 0: + S.append(i) + + value = cut_value(graph,flow,cut) + + f[s] = value + + for i in range(N): + if i == s: + continue + if i in S and p[i] == t: + p[i] = s + if p[t] in S: + p[s] = p[t] + p[t] = s + f[s] = f[t] + f[t] = value + + #cut tree is a dictionary, where each edge is associated with its weight + b = {} + for i in range(1,N): + b[(n[i],n[p[i]])] = f[i] + return b + diff --git a/core/build/lib/pygraph/algorithms/pagerank.py b/core/build/lib/pygraph/algorithms/pagerank.py new file mode 100644 index 0000000..1a72a22 --- /dev/null +++ b/core/build/lib/pygraph/algorithms/pagerank.py @@ -0,0 +1,76 @@ +# Copyright (c) 2010 Pedro Matiello +# Juarez Bochi +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, +# copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following +# conditions: + +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. + + +""" +PageRank algoritm + +@sort: pagerank +""" + +def pagerank(graph, damping_factor=0.85, max_iterations=100, min_delta=0.00001): + """ + Compute and return the PageRank in an directed graph. + + @type graph: digraph + @param graph: Digraph. + + @type damping_factor: number + @param damping_factor: PageRank dumping factor. + + @type max_iterations: number + @param max_iterations: Maximum number of iterations. + + @type min_delta: number + @param min_delta: Smallest variation required to have a new iteration. + + @rtype: Dict + @return: Dict containing all the nodes PageRank. + """ + + nodes = graph.nodes() + graph_size = len(nodes) + if graph_size == 0: + return {} + min_value = (1.0-damping_factor)/graph_size #value for nodes without inbound links + + # itialize the page rank dict with 1/N for all nodes + pagerank = dict.fromkeys(nodes, 1.0/graph_size) + + for i in range(max_iterations): + diff = 0 #total difference compared to last iteraction + # computes each node PageRank based on inbound links + for node in nodes: + rank = min_value + for referring_page in graph.incidents(node): + rank += damping_factor * pagerank[referring_page] / len(graph.neighbors(referring_page)) + + diff += abs(pagerank[node] - rank) + pagerank[node] = rank + + #stop if PageRank has converged + if diff < min_delta: + break + + return pagerank diff --git a/core/build/lib/pygraph/algorithms/searching.py b/core/build/lib/pygraph/algorithms/searching.py new file mode 100644 index 0000000..8cc1dfd --- /dev/null +++ b/core/build/lib/pygraph/algorithms/searching.py @@ -0,0 +1,153 @@ +# Copyright (c) 2007-2009 Pedro Matiello +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, +# copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following +# conditions: + +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. + + +""" +Search algorithms. + +@sort: breadth_first_search, depth_first_search +""" + + +# Imports +from pygraph.algorithms.filters.null import null +from sys import getrecursionlimit, setrecursionlimit + + +# Depth-first search + +def depth_first_search(graph, root=None, filter=null()): + """ + Depth-first search. + + @type graph: graph, digraph + @param graph: Graph. + + @type root: node + @param root: Optional root node (will explore only root's connected component) + + @rtype: tuple + @return: A tupple containing a dictionary and two lists: + 1. Generated spanning tree + 2. Graph's preordering + 3. Graph's postordering + """ + + recursionlimit = getrecursionlimit() + setrecursionlimit(max(len(graph.nodes())*2,recursionlimit)) + + def dfs(node): + """ + Depth-first search subfunction. + """ + visited[node] = 1 + pre.append(node) + # Explore recursively the connected component + for each in graph[node]: + if (each not in visited and list(filter(each, node))): + spanning_tree[each] = node + dfs(each) + post.append(node) + + visited = {} # List for marking visited and non-visited nodes + spanning_tree = {} # Spanning tree + pre = [] # Graph's preordering + post = [] # Graph's postordering + filter.configure(graph, spanning_tree) + + # DFS from one node only + if (root is not None): + if list(filter(root, None)): + spanning_tree[root] = None + dfs(root) + setrecursionlimit(recursionlimit) + return spanning_tree, pre, post + + # Algorithm loop + for each in graph: + # Select a non-visited node + if (each not in visited and list(filter(each, None))): + spanning_tree[each] = None + # Explore node's connected component + dfs(each) + + setrecursionlimit(recursionlimit) + + return (spanning_tree, pre, post) + + +# Breadth-first search + +def breadth_first_search(graph, root=None, filter=null()): + """ + Breadth-first search. + + @type graph: graph, digraph + @param graph: Graph. + + @type root: node + @param root: Optional root node (will explore only root's connected component) + + @rtype: tuple + @return: A tuple containing a dictionary and a list. + 1. Generated spanning tree + 2. Graph's level-based ordering + """ + + def bfs(): + """ + Breadth-first search subfunction. + """ + while (queue != []): + node = queue.pop(0) + + for other in graph[node]: + if (other not in spanning_tree and list(filter(other, node))): + queue.append(other) + ordering.append(other) + spanning_tree[other] = node + + queue = [] # Visiting queue + spanning_tree = {} # Spanning tree + ordering = [] + filter.configure(graph, spanning_tree) + + # BFS from one node only + if (root is not None): + if list(filter(root, None)): + queue.append(root) + ordering.append(root) + spanning_tree[root] = None + bfs() + return spanning_tree, ordering + + # Algorithm + for each in graph: + if (each not in spanning_tree): + if list(filter(each, None)): + queue.append(each) + ordering.append(each) + spanning_tree[each] = None + bfs() + + return spanning_tree, ordering diff --git a/core/build/lib/pygraph/algorithms/sorting.py b/core/build/lib/pygraph/algorithms/sorting.py new file mode 100644 index 0000000..8b97598 --- /dev/null +++ b/core/build/lib/pygraph/algorithms/sorting.py @@ -0,0 +1,51 @@ +# Copyright (c) 2007-2009 Pedro Matiello +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, +# copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following +# conditions: + +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. + + +""" +Sorting algorithms. + +@sort: topological_sorting +""" + + +# Imports +from pygraph.algorithms.searching import depth_first_search + +# Topological sorting +def topological_sorting(graph): + """ + Topological sorting. + + @attention: Topological sorting is meaningful only for directed acyclic graphs. + + @type graph: digraph + @param graph: Graph. + + @rtype: list + @return: Topological sorting for the graph. + """ + # The topological sorting of a DAG is equivalent to its reverse postordering. + order = depth_first_search(graph)[2] + order.reverse() + return order diff --git a/core/build/lib/pygraph/algorithms/traversal.py b/core/build/lib/pygraph/algorithms/traversal.py new file mode 100644 index 0000000..820ffb4 --- /dev/null +++ b/core/build/lib/pygraph/algorithms/traversal.py @@ -0,0 +1,84 @@ +# Copyright (c) 2008-2009 Pedro Matiello +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, +# copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following +# conditions: + +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. + + +""" +Traversal algorithms. + +@sort: traversal +""" + + +# Minimal spanning tree + +def traversal(graph, node, order): + """ + Graph traversal iterator. + + @type graph: graph, digraph + @param graph: Graph. + + @type node: node + @param node: Node. + + @type order: string + @param order: traversal ordering. Possible values are: + 2. 'pre' - Preordering (default) + 1. 'post' - Postordering + + @rtype: iterator + @return: Traversal iterator. + """ + visited = {} + if (order == 'pre'): + pre = 1 + post = 0 + elif (order == 'post'): + pre = 0 + post = 1 + + for each in _dfs(graph, visited, node, pre, post): + yield each + + +def _dfs(graph, visited, node, pre, post): + """ + Depth-first search subfunction for traversals. + + @type graph: graph, digraph + @param graph: Graph. + + @type visited: dictionary + @param visited: List of nodes (visited nodes are marked non-zero). + + @type node: node + @param node: Node to be explored by DFS. + """ + visited[node] = 1 + if (pre): yield node + # Explore recursively the connected component + for each in graph[node]: + if (each not in visited): + for other in _dfs(graph, visited, each, pre, post): + yield other + if (post): yield node diff --git a/core/build/lib/pygraph/algorithms/utils.py b/core/build/lib/pygraph/algorithms/utils.py new file mode 100644 index 0000000..1410f86 --- /dev/null +++ b/core/build/lib/pygraph/algorithms/utils.py @@ -0,0 +1,89 @@ +# Copyright (c) 2008-2009 Pedro Matiello +# Roy Smith +# Salim Fadhley +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, +# copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following +# conditions: + +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. + + +""" +Miscellaneous useful stuff. +""" + +# Imports +from heapq import heappush, heappop, heapify + + +# Priority Queue +class priority_queue: + """ + Priority queue. + """ + + def __init__(self, list=[]): + self.heap = [HeapItem(i, 0) for i in list] + heapify(self.heap) + + def __contains__(self, item): + for heap_item in self.heap: + if item == heap_item.item: + return True + return False + + def __len__(self): + return len(self.heap) + + def empty(self): + return len(self.heap) == 0 + + def insert(self, item, priority): + """ + Insert item into the queue, with the given priority. + """ + heappush(self.heap, HeapItem(item, priority)) + + def pop(self): + """ + Return the item with the lowest priority, and remove it from the queue. + """ + return heappop(self.heap).item + + def peek(self): + """ + Return the item with the lowest priority. The queue is unchanged. + """ + return self.heap[0].item + + def discard(self, item): + new_heap = [] + for heap_item in self.heap: + if item != heap_item.item: + new_heap.append(heap_item) + self.heap = new_heap + heapify(self.heap) + +class HeapItem: + def __init__(self, item, priority): + self.item = item + self.priority = priority + + def __cmp__(self, other): + return cmp(self.priority, other.priority) diff --git a/core/build/lib/pygraph/classes/__init__.py b/core/build/lib/pygraph/classes/__init__.py new file mode 100644 index 0000000..ee5fc46 --- /dev/null +++ b/core/build/lib/pygraph/classes/__init__.py @@ -0,0 +1,27 @@ +# Copyright (c) 2008-2009 Pedro Matiello +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, +# copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following +# conditions: + +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. + + +""" +Data structure classes. +""" \ No newline at end of file diff --git a/core/build/lib/pygraph/classes/digraph.py b/core/build/lib/pygraph/classes/digraph.py new file mode 100644 index 0000000..97b3442 --- /dev/null +++ b/core/build/lib/pygraph/classes/digraph.py @@ -0,0 +1,259 @@ +# Copyright (c) 2007-2009 Pedro Matiello +# Christian Muise +# Johannes Reinhardt +# Nathan Davis +# Zsolt Haraszti +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, +# copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following +# conditions: + +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. +""" +Digraph class +""" + +# Imports +from pygraph.classes.exceptions import AdditionError +from pygraph.mixins.labeling import labeling +from pygraph.mixins.common import common +from pygraph.mixins.basegraph import basegraph + +class digraph (basegraph, common, labeling): + """ + Digraph class. + + Digraphs are built of nodes and directed edges. + + @sort: __eq__, __init__, __ne__, add_edge, add_node, del_edge, del_node, edges, has_edge, has_node, + incidents, neighbors, node_order, nodes + """ + + DIRECTED = True + + def __init__(self): + """ + Initialize a digraph. + """ + common.__init__(self) + labeling.__init__(self) + self.node_neighbors = {} # Pairing: Node -> Neighbors + self.node_incidence = {} # Pairing: Node -> Incident nodes + + + def nodes(self): + """ + Return node list. + + @rtype: list + @return: Node list. + """ + return list(self.node_neighbors.keys()) + + + def neighbors(self, node): + """ + Return all nodes that are directly accessible from given node. + + @type node: node + @param node: Node identifier + + @rtype: list + @return: List of nodes directly accessible from given node. + """ + return self.node_neighbors[node] + + + def incidents(self, node): + """ + Return all nodes that are incident to the given node. + + @type node: node + @param node: Node identifier + + @rtype: list + @return: List of nodes directly accessible from given node. + """ + return self.node_incidence[node] + + def edges(self): + """ + Return all edges in the graph. + + @rtype: list + @return: List of all edges in the graph. + """ + return [ a for a in self._edges() ] + + def _edges(self): + for n, neighbors in list(self.node_neighbors.items()): + for neighbor in neighbors: + yield (n, neighbor) + + def has_node(self, node): + """ + Return whether the requested node exists. + + @type node: node + @param node: Node identifier + + @rtype: boolean + @return: Truth-value for node existence. + """ + return node in self.node_neighbors + + def add_node(self, node, attrs = None): + """ + Add given node to the graph. + + @attention: While nodes can be of any type, it's strongly recommended to use only + numbers and single-line strings as node identifiers if you intend to use write(). + + @type node: node + @param node: Node identifier. + + @type attrs: list + @param attrs: List of node attributes specified as (attribute, value) tuples. + """ + if attrs is None: + attrs = [] + if (node not in self.node_neighbors): + self.node_neighbors[node] = [] + self.node_incidence[node] = [] + self.node_attr[node] = attrs + else: + raise AdditionError("Node %s already in digraph" % node) + + + def add_edge(self, edge, wt = 1, label="", attrs = []): + """ + Add an directed edge to the graph connecting two nodes. + + An edge, here, is a pair of nodes like C{(n, m)}. + + @type edge: tuple + @param edge: Edge. + + @type wt: number + @param wt: Edge weight. + + @type label: string + @param label: Edge label. + + @type attrs: list + @param attrs: List of node attributes specified as (attribute, value) tuples. + """ + u, v = edge + for n in [u,v]: + if not n in self.node_neighbors: + raise AdditionError( "%s is missing from the node_neighbors table" % n ) + if not n in self.node_incidence: + raise AdditionError( "%s is missing from the node_incidence table" % n ) + + if v in self.node_neighbors[u] and u in self.node_incidence[v]: + raise AdditionError("Edge (%s, %s) already in digraph" % (u, v)) + else: + self.node_neighbors[u].append(v) + self.node_incidence[v].append(u) + self.set_edge_weight((u, v), wt) + self.add_edge_attributes( (u, v), attrs ) + self.set_edge_properties( (u, v), label=label, weight=wt ) + + + def del_node(self, node): + """ + Remove a node from the graph. + + @type node: node + @param node: Node identifier. + """ + for each in list(self.incidents(node)): + # Delete all the edges incident on this node + self.del_edge((each, node)) + + for each in list(self.neighbors(node)): + # Delete all the edges pointing to this node. + self.del_edge((node, each)) + + # Remove this node from the neighbors and incidents tables + del(self.node_neighbors[node]) + del(self.node_incidence[node]) + + # Remove any labeling which may exist. + self.del_node_labeling( node ) + + + def del_edge(self, edge): + """ + Remove an directed edge from the graph. + + @type edge: tuple + @param edge: Edge. + """ + u, v = edge + self.node_neighbors[u].remove(v) + self.node_incidence[v].remove(u) + self.del_edge_labeling( (u,v) ) + + + def has_edge(self, edge): + """ + Return whether an edge exists. + + @type edge: tuple + @param edge: Edge. + + @rtype: boolean + @return: Truth-value for edge existence. + """ + u, v = edge + return (u, v) in self.edge_properties + + + def node_order(self, node): + """ + Return the order of the given node. + + @rtype: number + @return: Order of the given node. + """ + return len(self.neighbors(node)) + + def __eq__(self, other): + """ + Return whether this graph is equal to another one. + + @type other: graph, digraph + @param other: Other graph or digraph + + @rtype: boolean + @return: Whether this graph and the other are equal. + """ + return common.__eq__(self, other) and labeling.__eq__(self, other) + + def __ne__(self, other): + """ + Return whether this graph is not equal to another one. + + @type other: graph, digraph + @param other: Other graph or digraph + + @rtype: boolean + @return: Whether this graph and the other are different. + """ + return not (self == other) diff --git a/core/build/lib/pygraph/classes/exceptions.py b/core/build/lib/pygraph/classes/exceptions.py new file mode 100644 index 0000000..1a3cd5f --- /dev/null +++ b/core/build/lib/pygraph/classes/exceptions.py @@ -0,0 +1,76 @@ +# Copyright (c) 2008-2009 Pedro Matiello +# Salim Fadhley +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, +# copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following +# conditions: + +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. + + +""" +Exceptions. +""" + +# Graph errors + +class GraphError(RuntimeError): + """ + A base-class for the various kinds of errors that occur in the the python-graph class. + """ + pass + +class AdditionError(GraphError): + """ + This error is raised when trying to add a node or edge already added to the graph or digraph. + """ + pass + +class NodeUnreachable(GraphError): + """ + Goal could not be reached from start. + """ + def __init__(self, start, goal): + msg = "Node %s could not be reached from node %s" % ( repr(goal), repr(start) ) + InvalidGraphType.__init__(self, msg) + self.start = start + self.goal = goal + +class InvalidGraphType(GraphError): + """ + Invalid graph type. + """ + pass + +# Algorithm errors + +class AlgorithmError(RuntimeError): + """ + A base-class for the various kinds of errors that occur in the the + algorithms package. + """ + pass + +class NegativeWeightCycleError(AlgorithmError): + """ + Algorithms like the Bellman-Ford algorithm can detect and raise an exception + when they encounter a negative weight cycle. + + @see: pygraph.algorithms.shortest_path_bellman_ford + """ + pass diff --git a/core/build/lib/pygraph/classes/graph.py b/core/build/lib/pygraph/classes/graph.py new file mode 100644 index 0000000..8aee642 --- /dev/null +++ b/core/build/lib/pygraph/classes/graph.py @@ -0,0 +1,230 @@ +# Copyright (c) 2007-2009 Pedro Matiello +# Johannes Reinhardt +# Nathan Davis +# Zsolt Haraszti +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, +# copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following +# conditions: + +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. + + +""" +Graph class +""" + + +# Imports +from pygraph.classes.exceptions import AdditionError +from pygraph.mixins.labeling import labeling +from pygraph.mixins.common import common +from pygraph.mixins.basegraph import basegraph + + +class graph(basegraph, common, labeling): + """ + Graph class. + + Graphs are built of nodes and edges. + + @sort: __eq__, __init__, __ne__, add_edge, add_node, del_edge, del_node, edges, has_edge, has_node, + neighbors, node_order, nodes + """ + + DIRECTED = False + + + def __init__(self): + """ + Initialize a graph. + """ + common.__init__(self) + labeling.__init__(self) + self.node_neighbors = {} # Pairing: Node -> Neighbors + + def nodes(self): + """ + Return node list. + + @rtype: list + @return: Node list. + """ + return list(self.node_neighbors.keys()) + + + def neighbors(self, node): + """ + Return all nodes that are directly accessible from given node. + + @type node: node + @param node: Node identifier + + @rtype: list + @return: List of nodes directly accessible from given node. + """ + return self.node_neighbors[node] + + def edges(self): + """ + Return all edges in the graph. + + @rtype: list + @return: List of all edges in the graph. + """ + return [ a for a in list(self.edge_properties.keys()) ] + + def has_node(self, node): + """ + Return whether the requested node exists. + + @type node: node + @param node: Node identifier + + @rtype: boolean + @return: Truth-value for node existence. + """ + return node in self.node_neighbors + + + def add_node(self, node, attrs=None): + """ + Add given node to the graph. + + @attention: While nodes can be of any type, it's strongly recommended to use only + numbers and single-line strings as node identifiers if you intend to use write(). + + @type node: node + @param node: Node identifier. + + @type attrs: list + @param attrs: List of node attributes specified as (attribute, value) tuples. + """ + if attrs is None: + attrs = [] + if (not node in self.node_neighbors): + self.node_neighbors[node] = [] + self.node_attr[node] = attrs + else: + raise AdditionError("Node %s already in graph" % node) + + def add_edge(self, edge, wt=1, label='', attrs=[]): + """ + Add an edge to the graph connecting two nodes. + + An edge, here, is a pair of nodes like C{(n, m)}. + + @type edge: tuple + @param edge: Edge. + + @type wt: number + @param wt: Edge weight. + + @type label: string + @param label: Edge label. + + @type attrs: list + @param attrs: List of node attributes specified as (attribute, value) tuples. + """ + u, v = edge + if (v not in self.node_neighbors[u] and u not in self.node_neighbors[v]): + self.node_neighbors[u].append(v) + if (u != v): + self.node_neighbors[v].append(u) + + self.add_edge_attributes((u,v), attrs) + self.set_edge_properties((u, v), label=label, weight=wt) + else: + raise AdditionError("Edge (%s, %s) already in graph" % (u, v)) + + + def del_node(self, node): + """ + Remove a node from the graph. + + @type node: node + @param node: Node identifier. + """ + for each in list(self.neighbors(node)): + if (each != node): + self.del_edge((each, node)) + del(self.node_neighbors[node]) + del(self.node_attr[node]) + + + def del_edge(self, edge): + """ + Remove an edge from the graph. + + @type edge: tuple + @param edge: Edge. + """ + u, v = edge + self.node_neighbors[u].remove(v) + self.del_edge_labeling((u, v)) + if (u != v): + self.node_neighbors[v].remove(u) + self.del_edge_labeling((v, u)) # TODO: This is redundant + + def has_edge(self, edge): + """ + Return whether an edge exists. + + @type edge: tuple + @param edge: Edge. + + @rtype: boolean + @return: Truth-value for edge existence. + """ + u,v = edge + return (u,v) in self.edge_properties and (v,u) in self.edge_properties + + + def node_order(self, node): + """ + Return the order of the graph + + @rtype: number + @return: Order of the given node. + """ + return len(self.neighbors(node)) + + + def __eq__(self, other): + """ + Return whether this graph is equal to another one. + + @type other: graph, digraph + @param other: Other graph or digraph + + @rtype: boolean + @return: Whether this graph and the other are equal. + """ + return common.__eq__(self, other) and labeling.__eq__(self, other) + + def __ne__(self, other): + """ + Return whether this graph is not equal to another one. + + @type other: graph, digraph + @param other: Other graph or digraph + + @rtype: boolean + @return: Whether this graph and the other are different. + """ + return not (self == other) diff --git a/core/build/lib/pygraph/classes/hypergraph.py b/core/build/lib/pygraph/classes/hypergraph.py new file mode 100644 index 0000000..4742247 --- /dev/null +++ b/core/build/lib/pygraph/classes/hypergraph.py @@ -0,0 +1,363 @@ +# Copyright (c) 2008-2009 Pedro Matiello +# Anand Jeyahar +# Christian Muise +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, +# copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following +# conditions: + +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. + + +""" +Hypergraph class +""" + + +# Imports +from pygraph.classes.graph import graph +from pygraph.classes.exceptions import AdditionError + +from pygraph.mixins.labeling import labeling +from pygraph.mixins.common import common +from pygraph.mixins.basegraph import basegraph + +class hypergraph (basegraph, common, labeling): + """ + Hypergraph class. + + Hypergraphs are a generalization of graphs where an edge (called hyperedge) can connect more + than two nodes. + + @sort: __init__, __len__, __str__, add_hyperedge, add_hyperedges, add_node, add_nodes, + del_edge, has_node, has_edge, has_hyperedge, hyperedges, link, links, nodes, unlink + """ + + # Technically this isn't directed, but it gives us the right + # behaviour with the parent classes. + DIRECTED = True + + def __init__(self): + """ + Initialize a hypergraph. + """ + common.__init__(self) + labeling.__init__(self) + self.node_links = {} # Pairing: Node -> Hyperedge + self.edge_links = {} # Pairing: Hyperedge -> Node + self.graph = graph() # Ordinary graph + + + def nodes(self): + """ + Return node list. + + @rtype: list + @return: Node list. + """ + return list(self.node_links.keys()) + + + def edges(self): + """ + Return the hyperedge list. + + @rtype: list + @return: List of hyperedges in the graph. + """ + return self.hyperedges() + + + def hyperedges(self): + """ + Return hyperedge list. + + @rtype: list + @return: List of hyperedges in the graph. + """ + return list(self.edge_links.keys()) + + + def has_edge(self, hyperedge): + """ + Return whether the requested node exists. + + @type hyperedge: hyperedge + @param hyperedge: Hyperedge identifier + + @rtype: boolean + @return: Truth-value for hyperedge existence. + """ + return self.has_hyperedge(hyperedge) + + + def has_hyperedge(self, hyperedge): + """ + Return whether the requested node exists. + + @type hyperedge: hyperedge + @param hyperedge: Hyperedge identifier + + @rtype: boolean + @return: Truth-value for hyperedge existence. + """ + return hyperedge in self.edge_links + + + def links(self, obj): + """ + Return all nodes connected by the given hyperedge or all hyperedges + connected to the given hypernode. + + @type obj: hyperedge + @param obj: Object identifier. + + @rtype: list + @return: List of node objects linked to the given hyperedge. + """ + if obj in self.edge_links: + return self.edge_links[obj] + else: + return self.node_links[obj] + + + def neighbors(self, obj): + """ + Return all neighbors adjacent to the given node. + + @type obj: node + @param obj: Object identifier. + + @rtype: list + @return: List of all node objects adjacent to the given node. + """ + neighbors = set([]) + + for e in self.node_links[obj]: + neighbors.update(set(self.edge_links[e])) + + return list(neighbors - set([obj])) + + + def has_node(self, node): + """ + Return whether the requested node exists. + + @type node: node + @param node: Node identifier + + @rtype: boolean + @return: Truth-value for node existence. + """ + return node in self.node_links + + + def add_node(self, node): + """ + Add given node to the hypergraph. + + @attention: While nodes can be of any type, it's strongly recommended to use only numbers + and single-line strings as node identifiers if you intend to use write(). + + @type node: node + @param node: Node identifier. + """ + if (not node in self.node_links): + self.node_links[node] = [] + self.node_attr[node] = [] + self.graph.add_node((node,'n')) + else: + raise AdditionError("Node %s already in graph" % node) + + + def del_node(self, node): + """ + Delete a given node from the hypergraph. + + @type node: node + @param node: Node identifier. + """ + if self.has_node(node): + for e in self.node_links[node]: + self.edge_links[e].remove(node) + + self.node_links.pop(node) + self.graph.del_node((node,'n')) + + + def add_edge(self, hyperedge): + """ + Add given hyperedge to the hypergraph. + + @attention: While hyperedge-nodes can be of any type, it's strongly recommended to use only + numbers and single-line strings as node identifiers if you intend to use write(). + + @type hyperedge: hyperedge + @param hyperedge: Hyperedge identifier. + """ + self.add_hyperedge(hyperedge) + + + def add_hyperedge(self, hyperedge): + """ + Add given hyperedge to the hypergraph. + + @attention: While hyperedge-nodes can be of any type, it's strongly recommended to use only + numbers and single-line strings as node identifiers if you intend to use write(). + + @type hyperedge: hyperedge + @param hyperedge: Hyperedge identifier. + """ + if (not hyperedge in self.edge_links): + self.edge_links[hyperedge] = [] + self.graph.add_node((hyperedge,'h')) + + + def add_edges(self, edgelist): + """ + Add given hyperedges to the hypergraph. + + @attention: While hyperedge-nodes can be of any type, it's strongly recommended to use only + numbers and single-line strings as node identifiers if you intend to use write(). + + @type edgelist: list + @param edgelist: List of hyperedge-nodes to be added to the graph. + """ + self.add_hyperedges(edgelist) + + + def add_hyperedges(self, edgelist): + """ + Add given hyperedges to the hypergraph. + + @attention: While hyperedge-nodes can be of any type, it's strongly recommended to use only + numbers and single-line strings as node identifiers if you intend to use write(). + + @type edgelist: list + @param edgelist: List of hyperedge-nodes to be added to the graph. + """ + for each in edgelist: + self.add_hyperedge(each) + + + def del_edge(self, hyperedge): + """ + Delete the given hyperedge. + + @type hyperedge: hyperedge + @param hyperedge: Hyperedge identifier. + """ + self.del_hyperedge(hyperedge) + + + def del_hyperedge(self, hyperedge): + """ + Delete the given hyperedge. + + @type hyperedge: hyperedge + @param hyperedge: Hyperedge identifier. + """ + if (hyperedge in self.hyperedges()): + for n in self.edge_links[hyperedge]: + self.node_links[n].remove(hyperedge) + + del(self.edge_links[hyperedge]) + self.del_edge_labeling(hyperedge) + self.graph.del_node((hyperedge,'h')) + + + def link(self, node, hyperedge): + """ + Link given node and hyperedge. + + @type node: node + @param node: Node. + + @type hyperedge: node + @param hyperedge: Hyperedge. + """ + if (hyperedge not in self.node_links[node]): + self.edge_links[hyperedge].append(node) + self.node_links[node].append(hyperedge) + self.graph.add_edge(((node,'n'), (hyperedge,'h'))) + else: + raise AdditionError("Link (%s, %s) already in graph" % (node, hyperedge)) + + + def unlink(self, node, hyperedge): + """ + Unlink given node and hyperedge. + + @type node: node + @param node: Node. + + @type hyperedge: hyperedge + @param hyperedge: Hyperedge. + """ + self.node_links[node].remove(hyperedge) + self.edge_links[hyperedge].remove(node) + self.graph.del_edge(((node,'n'), (hyperedge,'h'))) + + + def rank(self): + """ + Return the rank of the given hypergraph. + + @rtype: int + @return: Rank of graph. + """ + max_rank = 0 + + for each in self.hyperedges(): + if len(self.edge_links[each]) > max_rank: + max_rank = len(self.edge_links[each]) + + return max_rank + + def __eq__(self, other): + """ + Return whether this hypergraph is equal to another one. + + @type other: hypergraph + @param other: Other hypergraph + + @rtype: boolean + @return: Whether this hypergraph and the other are equal. + """ + def links_eq(): + for edge in self.edges(): + for link in self.links(edge): + if (link not in other.links(edge)): return False + for edge in other.edges(): + for link in other.links(edge): + if (link not in self.links(edge)): return False + return True + + return common.__eq__(self, other) and links_eq() and labeling.__eq__(self, other) + + def __ne__(self, other): + """ + Return whether this hypergraph is not equal to another one. + + @type other: hypergraph + @param other: Other hypergraph + + @rtype: boolean + @return: Whether this hypergraph and the other are different. + """ + return not (self == other) \ No newline at end of file diff --git a/core/build/lib/pygraph/mixins/__init__.py b/core/build/lib/pygraph/mixins/__init__.py new file mode 100644 index 0000000..54c514e --- /dev/null +++ b/core/build/lib/pygraph/mixins/__init__.py @@ -0,0 +1,33 @@ +# Copyright (c) 2008-2009 Pedro Matiello +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, +# copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following +# conditions: + +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. + + +""" +Mixins. + +Base classes used to compose the the graph classes. + +The classes in this namespace should not be used directly. +""" + +__import__('pkg_resources').declare_namespace(__name__) diff --git a/core/build/lib/pygraph/mixins/basegraph.py b/core/build/lib/pygraph/mixins/basegraph.py new file mode 100644 index 0000000..694d2bc --- /dev/null +++ b/core/build/lib/pygraph/mixins/basegraph.py @@ -0,0 +1,32 @@ +# Copyright (c) 2008-2009 Pedro Matiello +# Salim Fadhley +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, +# copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following +# conditions: + +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. + + +class basegraph( object ): + """ + An abstract class intended as a common ancestor to all graph classes. This allows the user + to test isinstance(X, basegraph) to determine if the object is one of any of the python-graph + main classes. + """ + \ No newline at end of file diff --git a/core/build/lib/pygraph/mixins/common.py b/core/build/lib/pygraph/mixins/common.py new file mode 100644 index 0000000..c989967 --- /dev/null +++ b/core/build/lib/pygraph/mixins/common.py @@ -0,0 +1,215 @@ +# Copyright (c) 2008-2009 Pedro Matiello +# Salim Fadhley +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, +# copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following +# conditions: + +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. + + +class common( object ): + """ + Standard methods common to all graph classes. + + @sort: __eq__, __getitem__, __iter__, __len__, __repr__, __str__, add_graph, add_nodes, + add_spanning_tree, complete, inverse, order, reverse + """ + + def __str__(self): + """ + Return a string representing the graph when requested by str() (or print). + + @rtype: string + @return: String representing the graph. + """ + str_nodes = repr( self.nodes() ) + str_edges = repr( self.edges() ) + return "%s %s" % ( str_nodes, str_edges ) + + def __repr__(self): + """ + Return a string representing the graph when requested by repr() + + @rtype: string + @return: String representing the graph. + """ + return "<%s.%s %s>" % ( self.__class__.__module__, self.__class__.__name__, str(self) ) + + def __iter__(self): + """ + Return a iterator passing through all nodes in the graph. + + @rtype: iterator + @return: Iterator passing through all nodes in the graph. + """ + for n in self.nodes(): + yield n + + def __len__(self): + """ + Return the order of self when requested by len(). + + @rtype: number + @return: Size of the graph. + """ + return self.order() + + def __getitem__(self, node): + """ + Return a iterator passing through all neighbors of the given node. + + @rtype: iterator + @return: Iterator passing through all neighbors of the given node. + """ + for n in self.neighbors( node ): + yield n + + def order(self): + """ + Return the order of self, this is defined as the number of nodes in the graph. + + @rtype: number + @return: Size of the graph. + """ + return len(self.nodes()) + + def add_nodes(self, nodelist): + """ + Add given nodes to the graph. + + @attention: While nodes can be of any type, it's strongly recommended to use only + numbers and single-line strings as node identifiers if you intend to use write(). + Objects used to identify nodes absolutely must be hashable. If you need attach a mutable + or non-hashable node, consider using the labeling feature. + + @type nodelist: list + @param nodelist: List of nodes to be added to the graph. + """ + for each in nodelist: + self.add_node(each) + + def add_graph(self, other): + """ + Add other graph to this graph. + + @attention: Attributes and labels are not preserved. + + @type other: graph + @param other: Graph + """ + self.add_nodes( n for n in other.nodes() if not n in self.nodes() ) + + for each_node in other.nodes(): + for each_edge in other.neighbors(each_node): + if (not self.has_edge((each_node, each_edge))): + self.add_edge((each_node, each_edge)) + + + def add_spanning_tree(self, st): + """ + Add a spanning tree to the graph. + + @type st: dictionary + @param st: Spanning tree. + """ + self.add_nodes(list(st.keys())) + for each in st: + if (st[each] is not None): + self.add_edge((st[each], each)) + + + def complete(self): + """ + Make the graph a complete graph. + + @attention: This will modify the current graph. + """ + for each in self.nodes(): + for other in self.nodes(): + if (each != other and not self.has_edge((each, other))): + self.add_edge((each, other)) + + + def inverse(self): + """ + Return the inverse of the graph. + + @rtype: graph + @return: Complement graph for the graph. + """ + inv = self.__class__() + inv.add_nodes(self.nodes()) + inv.complete() + for each in self.edges(): + if (inv.has_edge(each)): + inv.del_edge(each) + return inv + + def reverse(self): + """ + Generate the reverse of a directed graph, returns an identical graph if not directed. + Attributes & weights are preserved. + + @rtype: digraph + @return: The directed graph that should be reversed. + """ + assert self.DIRECTED, "Undirected graph types such as %s cannot be reversed" % self.__class__.__name__ + + N = self.__class__() + + #- Add the nodes + N.add_nodes( n for n in self.nodes() ) + + #- Add the reversed edges + for (u, v) in self.edges(): + wt = self.edge_weight((u, v)) + label = self.edge_label((u, v)) + attributes = self.edge_attributes((u, v)) + N.add_edge((v, u), wt, label, attributes) + return N + + def __eq__(self, other): + """ + Return whether this graph is equal to another one. + + @type other: graph, digraph + @param other: Other graph or digraph + + @rtype: boolean + @return: Whether this graph and the other are equal. + """ + + def nodes_eq(): + for each in self: + if (not other.has_node(each)): return False + for each in other: + if (not self.has_node(each)): return False + return True + + def edges_eq(): + for edge in self.edges(): + if (not other.has_edge(edge)): return False + for edge in other.edges(): + if (not self.has_edge(edge)): return False + return True + + try: + return nodes_eq() and edges_eq() + except AttributeError: + return False diff --git a/core/build/lib/pygraph/mixins/labeling.py b/core/build/lib/pygraph/mixins/labeling.py new file mode 100644 index 0000000..3340ddd --- /dev/null +++ b/core/build/lib/pygraph/mixins/labeling.py @@ -0,0 +1,227 @@ +# Copyright (c) 2008-2009 Pedro Matiello +# Salim Fadhley +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, +# copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following +# conditions: + +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. + + +class labeling( object ): + """ + Generic labeling support for graphs + + @sort: __eq__, __init__, add_edge_attribute, add_edge_attributes, add_node_attribute, + del_edge_labeling, del_node_labeling, edge_attributes, edge_label, edge_weight, + get_edge_properties, node_attributes, set_edge_label, set_edge_properties, set_edge_weight + """ + WEIGHT_ATTRIBUTE_NAME = "weight" + DEFAULT_WEIGHT = 1 + + LABEL_ATTRIBUTE_NAME = "label" + DEFAULT_LABEL = "" + + def __init__(self): + # Metadata bout edges + self.edge_properties = {} # Mapping: Edge -> Dict mapping, lablel-> str, wt->num + self.edge_attr = {} # Key value pairs: (Edge -> Attributes) + + # Metadata bout nodes + self.node_attr = {} # Pairing: Node -> Attributes + + def del_node_labeling( self, node ): + if node in self.node_attr: + # Since attributes and properties are lazy, they might not exist. + del( self.node_attr[node] ) + + def del_edge_labeling( self, edge ): + + keys = [edge] + if not self.DIRECTED: + keys.append(edge[::-1]) + + for key in keys: + for mapping in [self.edge_properties, self.edge_attr ]: + try: + del ( mapping[key] ) + except KeyError: + pass + + def edge_weight(self, edge): + """ + Get the weight of an edge. + + @type edge: edge + @param edge: One edge. + + @rtype: number + @return: Edge weight. + """ + return self.get_edge_properties( edge ).setdefault( self.WEIGHT_ATTRIBUTE_NAME, self.DEFAULT_WEIGHT ) + + + def set_edge_weight(self, edge, wt): + """ + Set the weight of an edge. + + @type edge: edge + @param edge: One edge. + + @type wt: number + @param wt: Edge weight. + """ + self.set_edge_properties(edge, weight=wt ) + if not self.DIRECTED: + self.set_edge_properties((edge[1], edge[0]) , weight=wt ) + + + def edge_label(self, edge): + """ + Get the label of an edge. + + @type edge: edge + @param edge: One edge. + + @rtype: string + @return: Edge label + """ + return self.get_edge_properties( edge ).setdefault( self.LABEL_ATTRIBUTE_NAME, self.DEFAULT_LABEL ) + + def set_edge_label(self, edge, label): + """ + Set the label of an edge. + + @type edge: edge + @param edge: One edge. + + @type label: string + @param label: Edge label. + """ + self.set_edge_properties(edge, label=label ) + if not self.DIRECTED: + self.set_edge_properties((edge[1], edge[0]) , label=label ) + + def set_edge_properties(self, edge, **properties ): + self.edge_properties.setdefault( edge, {} ).update( properties ) + if (not self.DIRECTED and edge[0] != edge[1]): + self.edge_properties.setdefault((edge[1], edge[0]), {}).update( properties ) + + def get_edge_properties(self, edge): + return self.edge_properties.setdefault( edge, {} ) + + def add_edge_attribute(self, edge, attr): + """ + Add attribute to the given edge. + + @type edge: edge + @param edge: One edge. + + @type attr: tuple + @param attr: Node attribute specified as a tuple in the form (attribute, value). + """ + self.edge_attr[edge] = self.edge_attributes(edge) + [attr] + + if (not self.DIRECTED and edge[0] != edge[1]): + self.edge_attr[(edge[1],edge[0])] = self.edge_attributes((edge[1], edge[0])) + [attr] + + def add_edge_attributes(self, edge, attrs): + """ + Append a sequence of attributes to the given edge + + @type edge: edge + @param edge: One edge. + + @type attrs: tuple + @param attrs: Node attributes specified as a sequence of tuples in the form (attribute, value). + """ + for attr in attrs: + self.add_edge_attribute(edge, attr) + + + def add_node_attribute(self, node, attr): + """ + Add attribute to the given node. + + @type node: node + @param node: Node identifier + + @type attr: tuple + @param attr: Node attribute specified as a tuple in the form (attribute, value). + """ + self.node_attr[node] = self.node_attr[node] + [attr] + + + def node_attributes(self, node): + """ + Return the attributes of the given node. + + @type node: node + @param node: Node identifier + + @rtype: list + @return: List of attributes specified tuples in the form (attribute, value). + """ + return self.node_attr[node] + + + def edge_attributes(self, edge): + """ + Return the attributes of the given edge. + + @type edge: edge + @param edge: One edge. + + @rtype: list + @return: List of attributes specified tuples in the form (attribute, value). + """ + try: + return self.edge_attr[edge] + except KeyError: + return [] + + def __eq__(self, other): + """ + Return whether this graph is equal to another one. + + @type other: graph, digraph + @param other: Other graph or digraph + + @rtype: boolean + @return: Whether this graph and the other are equal. + """ + def attrs_eq(list1, list2): + for each in list1: + if (each not in list2): return False + for each in list2: + if (each not in list1): return False + return True + + def edges_eq(): + for edge in self.edges(): + if (self.edge_weight(edge) != other.edge_weight(edge)): return False + if (self.edge_label(edge) != other.edge_label(edge)): return False + if (not attrs_eq(self.edge_attributes(edge), other.edge_attributes(edge))): return False + return True + + def nodes_eq(): + for node in self: + if (not attrs_eq(self.node_attributes(node), other.node_attributes(node))): return False + return True + + return nodes_eq() and edges_eq() \ No newline at end of file diff --git a/core/build/lib/pygraph/readwrite/__init__.py b/core/build/lib/pygraph/readwrite/__init__.py new file mode 100644 index 0000000..01815a4 --- /dev/null +++ b/core/build/lib/pygraph/readwrite/__init__.py @@ -0,0 +1,31 @@ +# Copyright (c) 2008-2009 Pedro Matiello +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, +# copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following +# conditions: + +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. + + +""" +Readwrite algorithms. + +Algorithms for reading and writing graphs. +""" + +__import__('pkg_resources').declare_namespace(__name__) \ No newline at end of file diff --git a/core/build/lib/pygraph/readwrite/markup.py b/core/build/lib/pygraph/readwrite/markup.py new file mode 100644 index 0000000..c6e2455 --- /dev/null +++ b/core/build/lib/pygraph/readwrite/markup.py @@ -0,0 +1,195 @@ +# Copyright (c) 2007-2009 Pedro Matiello +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, +# copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following +# conditions: + +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. + + +""" +Functions for reading and writing graphs in a XML markup. + +@sort: read, read_hypergraph, write, write_hypergraph +""" + + +# Imports +from pygraph.classes.digraph import digraph +from pygraph.classes.exceptions import InvalidGraphType +from pygraph.classes.graph import graph +from pygraph.classes.hypergraph import hypergraph +from xml.dom.minidom import Document, parseString + + +def write(G): + """ + Return a string specifying the given graph as a XML document. + + @type G: graph + @param G: Graph. + + @rtype: string + @return: String specifying the graph as a XML document. + """ + + # Document root + grxml = Document() + if (type(G) == graph): + grxmlr = grxml.createElement('graph') + elif (type(G) == digraph ): + grxmlr = grxml.createElement('digraph') + elif (type(G) == hypergraph ): + return write_hypergraph(G) + else: + raise InvalidGraphType + grxml.appendChild(grxmlr) + + # Each node... + for each_node in G.nodes(): + node = grxml.createElement('node') + node.setAttribute('id', str(each_node)) + grxmlr.appendChild(node) + for each_attr in G.node_attributes(each_node): + attr = grxml.createElement('attribute') + attr.setAttribute('attr', each_attr[0]) + attr.setAttribute('value', each_attr[1]) + node.appendChild(attr) + + # Each edge... + for edge_from, edge_to in G.edges(): + edge = grxml.createElement('edge') + edge.setAttribute('from', str(edge_from)) + edge.setAttribute('to', str(edge_to)) + edge.setAttribute('wt', str(G.edge_weight((edge_from, edge_to)))) + edge.setAttribute('label', str(G.edge_label((edge_from, edge_to)))) + grxmlr.appendChild(edge) + for attr_name, attr_value in G.edge_attributes((edge_from, edge_to)): + attr = grxml.createElement('attribute') + attr.setAttribute('attr', attr_name) + attr.setAttribute('value', attr_value) + edge.appendChild(attr) + + return grxml.toprettyxml() + + +def read(string): + """ + Read a graph from a XML document and return it. Nodes and edges specified in the input will + be added to the current graph. + + @type string: string + @param string: Input string in XML format specifying a graph. + + @rtype: graph + @return: Graph + """ + dom = parseString(string) + if dom.getElementsByTagName("graph"): + G = graph() + elif dom.getElementsByTagName("digraph"): + G = digraph() + elif dom.getElementsByTagName("hypergraph"): + return read_hypergraph(string) + else: + raise InvalidGraphType + + # Read nodes... + for each_node in dom.getElementsByTagName("node"): + G.add_node(each_node.getAttribute('id')) + for each_attr in each_node.getElementsByTagName("attribute"): + G.add_node_attribute(each_node.getAttribute('id'), + (each_attr.getAttribute('attr'), + each_attr.getAttribute('value'))) + + # Read edges... + for each_edge in dom.getElementsByTagName("edge"): + if (not G.has_edge((each_edge.getAttribute('from'), each_edge.getAttribute('to')))): + G.add_edge((each_edge.getAttribute('from'), each_edge.getAttribute('to')), \ + wt = float(each_edge.getAttribute('wt')), label = each_edge.getAttribute('label')) + for each_attr in each_edge.getElementsByTagName("attribute"): + attr_tuple = (each_attr.getAttribute('attr'), each_attr.getAttribute('value')) + if (attr_tuple not in G.edge_attributes((each_edge.getAttribute('from'), \ + each_edge.getAttribute('to')))): + G.add_edge_attribute((each_edge.getAttribute('from'), \ + each_edge.getAttribute('to')), attr_tuple) + + return G + + +def write_hypergraph(hgr): + """ + Return a string specifying the given hypergraph as a XML document. + + @type hgr: hypergraph + @param hgr: Hypergraph. + + @rtype: string + @return: String specifying the graph as a XML document. + """ + + # Document root + grxml = Document() + grxmlr = grxml.createElement('hypergraph') + grxml.appendChild(grxmlr) + + # Each node... + nodes = hgr.nodes() + hyperedges = hgr.hyperedges() + for each_node in (nodes + hyperedges): + if (each_node in nodes): + node = grxml.createElement('node') + else: + node = grxml.createElement('hyperedge') + node.setAttribute('id', str(each_node)) + grxmlr.appendChild(node) + + # and its outgoing edge + if each_node in nodes: + for each_edge in hgr.links(each_node): + edge = grxml.createElement('link') + edge.setAttribute('to', str(each_edge)) + node.appendChild(edge) + + return grxml.toprettyxml() + + +def read_hypergraph(string): + """ + Read a graph from a XML document. Nodes and hyperedges specified in the input will be added + to the current graph. + + @type string: string + @param string: Input string in XML format specifying a graph. + + @rtype: hypergraph + @return: Hypergraph + """ + + hgr = hypergraph() + + dom = parseString(string) + for each_node in dom.getElementsByTagName("node"): + hgr.add_node(each_node.getAttribute('id')) + for each_node in dom.getElementsByTagName("hyperedge"): + hgr.add_hyperedge(each_node.getAttribute('id')) + dom = parseString(string) + for each_node in dom.getElementsByTagName("node"): + for each_edge in each_node.getElementsByTagName("link"): + hgr.link(str(each_node.getAttribute('id')), str(each_edge.getAttribute('to'))) + return hgr diff --git a/core/dist/python_graph_core-1.8.2-py3.5.egg b/core/dist/python_graph_core-1.8.2-py3.5.egg new file mode 100644 index 0000000000000000000000000000000000000000..f9fe0f276a955284092ebcd610057cc17ff4ee9f GIT binary patch literal 95316 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%0D@A+%ONI;S>k|=$OJW#-i5HdXJ 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

srrjMl5aj}$0$!7QGc%+ z6L($UXOhBgo{;cy-Yh4*6j%N+0rg9V$`!?o`z}c>!;>N{EW6`cd-Wdm&%zSEGEKbZ zRp(O|7v=xwd5HTRQ_c15%e`)y%iDA`NJtqWj39cqL(nlDOWAt}ES7+P;o#SEH2C$a zppfWaDp071x|XW8m^H!;;bM(&f%{p4Wtfg0qMp8<_^6JYpS55bG5|v$oan267Zg$+ z{Dncez?=~tZZJ5)68z%`vqcbuH9os})e#tl1pN&>vS11dd3iHTN8?`u@oo8hf#in> zxI7G;r-rWVMnNGt0T5%0zV;ctlPdazYE$7+(k}rnI}LuAHZBLhQOHZcUl@X?tDB`0 z!W-d4h*5YsgAo`20g`x?nAps?VDBd40kc7Dt_1o+#iAS6lQ?kwF>qcM1C9wy;D6ox zer>KJh8c0({O|!6m6S5BgH1$J4?-ZkH~{qYn+*WW$<`Ho_ONsI5QD+t2oDc?D|;t< zPe1Ve10QI@OignNT*}J~fq;d&n1Vv)0WT8(!U5PG4s#;FGU|?fa1D&4T{|EUu#*X< zppatVg$^N>AKVE93$9h}CU#H61qePJ9fq=Qg?fzXdjo*wvX2e!AK?J@7K=jMAje` zx_c5vxsa&5jRMj55yU8Nr+)@|4gqg@y2HE??j8g~BF;AMLn44p1Bao%%xx4DGEtWx z%*)f>3BL`|o@baNMNxwa3IgxrdfwKMAc#mO8u3tz;RQy012qKUgefTGeG`H#8+#{D zP(v{rdlzfGmPFfjP9V+THL$G!^dnr}E(@YO7cVD5{b*NMdPF|3Zyj{>{TS=e(a&c^ zl;;k!w)Z0By<92QCu5`#$Xh_=$06>vB})8@7t!~bAhph8LxAZILQWcoDe15U6PsN9 z@+fX9x;eDbI?cX|^1(nS6SO?8j)U`m*7@aW;yM-*@zV346@x)5%Hq^V+3i2;{B8l_ zN=K^--wXo<3UE7<$5FcO`Ja_AE%SFRbZl0)KG|mkJVy;;i4RBXi5EdFJA{|Jy$2ZL zJ;dO4u0909o$lx``8|Lf2gAG|4)UD;R!D>w+{xY=0YmR*wYj0urp|?Y8Q=j8U;@}+ zT*ed>QWLoq7vl|FEINLW1Cy$^LEP(sb_Ihprl63h(5~0B5Qq$p zoMtpZxy_GJxQPgHAs+v|*Y<*86BptNqj9mp=+l1>0+$`bQGngsQejg#x--!-! zg)7Ucqw`<@CI$Txw>6?`w^YC+K*TkE)S3-(0X_p5rlfFu^{ReL4QygWTx6WC&O8%z zl>$&1^qZ26fA3@fgG7xiN+ zQP+*t*w|Zjr?m!m33HIC zf@7>O1%;e_`zLB+BUJp`m~=(>r$@jTh%mlES3dkn*>utau0iO!;h!PCVG#-$xcDau z<9J*U`o@HRMl%*<_k|D$d)M1=Jn?|RKhO`$PhKO=-_#()CGZaz!%Dc2l3*T7HsWRT zorlnIW1AZd?I8TaIk5C-8shX#4M@BK_}d3#C3L|)yc^F|gerh(*uM*)qXmESUaY_o z7OcSUB{|py{kiefpfkln#5=LR8g5ZRT&9="+version) + return + except pkg_resources.VersionConflict: + e = sys.exc_info()[1] + if was_imported: + sys.stderr.write( + "The required version of distribute (>=%s) is not available,\n" + "and can't be installed while this script is running. Please\n" + "install a more recent version first, using\n" + "'easy_install -U distribute'." + "\n\n(Currently using %r)\n" % (version, e.args[0])) + sys.exit(2) + else: + del pkg_resources, sys.modules['pkg_resources'] # reload ok + return _do_download(version, download_base, to_dir, + download_delay) + except pkg_resources.DistributionNotFound: + return _do_download(version, download_base, to_dir, + download_delay) + finally: + if not no_fake: + _create_fake_setuptools_pkg_info(to_dir) + +def download_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL, + to_dir=os.curdir, delay=15): + """Download distribute from a specified location and return its filename + + `version` should be a valid distribute version number that is available + as an egg for download under the `download_base` URL (which should end + with a '/'). `to_dir` is the directory where the egg will be downloaded. + `delay` is the number of seconds to pause before an actual download + attempt. + """ + # making sure we use the absolute path + to_dir = os.path.abspath(to_dir) + try: + from urllib.request import urlopen + except ImportError: + from urllib2 import urlopen + tgz_name = "distribute-%s.tar.gz" % version + url = download_base + tgz_name + saveto = os.path.join(to_dir, tgz_name) + src = dst = None + if not os.path.exists(saveto): # Avoid repeated downloads + try: + log.warn("Downloading %s", url) + src = urlopen(url) + # Read/write all in one block, so we don't create a corrupt file + # if the download is interrupted. + data = src.read() + dst = open(saveto, "wb") + dst.write(data) + finally: + if src: + src.close() + if dst: + dst.close() + return os.path.realpath(saveto) + +def _no_sandbox(function): + def __no_sandbox(*args, **kw): + try: + from setuptools.sandbox import DirectorySandbox + if not hasattr(DirectorySandbox, '_old'): + def violation(*args): + pass + DirectorySandbox._old = DirectorySandbox._violation + DirectorySandbox._violation = violation + patched = True + else: + patched = False + except ImportError: + patched = False + + try: + return function(*args, **kw) + finally: + if patched: + DirectorySandbox._violation = DirectorySandbox._old + del DirectorySandbox._old + + return __no_sandbox + +def _patch_file(path, content): + """Will backup the file then patch it""" + existing_content = open(path).read() + if existing_content == content: + # already patched + log.warn('Already patched.') + return False + log.warn('Patching...') + _rename_path(path) + f = open(path, 'w') + try: + f.write(content) + finally: + f.close() + return True + +_patch_file = _no_sandbox(_patch_file) + +def _same_content(path, content): + return open(path).read() == content + +def _rename_path(path): + new_name = path + '.OLD.%s' % time.time() + log.warn('Renaming %s into %s', path, new_name) + os.rename(path, new_name) + return new_name + +def _remove_flat_installation(placeholder): + if not os.path.isdir(placeholder): + log.warn('Unkown installation at %s', placeholder) + return False + found = False + for file in os.listdir(placeholder): + if fnmatch.fnmatch(file, 'setuptools*.egg-info'): + found = True + break + if not found: + log.warn('Could not locate setuptools*.egg-info') + return + + log.warn('Removing elements out of the way...') + pkg_info = os.path.join(placeholder, file) + if os.path.isdir(pkg_info): + patched = _patch_egg_dir(pkg_info) + else: + patched = _patch_file(pkg_info, SETUPTOOLS_PKG_INFO) + + if not patched: + log.warn('%s already patched.', pkg_info) + return False + # now let's move the files out of the way + for element in ('setuptools', 'pkg_resources.py', 'site.py'): + element = os.path.join(placeholder, element) + if os.path.exists(element): + _rename_path(element) + else: + log.warn('Could not find the %s element of the ' + 'Setuptools distribution', element) + return True + +_remove_flat_installation = _no_sandbox(_remove_flat_installation) + +def _after_install(dist): + log.warn('After install bootstrap.') + placeholder = dist.get_command_obj('install').install_purelib + _create_fake_setuptools_pkg_info(placeholder) + +def _create_fake_setuptools_pkg_info(placeholder): + if not placeholder or not os.path.exists(placeholder): + log.warn('Could not find the install location') + return + pyver = '%s.%s' % (sys.version_info[0], sys.version_info[1]) + setuptools_file = 'setuptools-%s-py%s.egg-info' % \ + (SETUPTOOLS_FAKED_VERSION, pyver) + pkg_info = os.path.join(placeholder, setuptools_file) + if os.path.exists(pkg_info): + log.warn('%s already exists', pkg_info) + return + + log.warn('Creating %s', pkg_info) + f = open(pkg_info, 'w') + try: + f.write(SETUPTOOLS_PKG_INFO) + finally: + f.close() + + pth_file = os.path.join(placeholder, 'setuptools.pth') + log.warn('Creating %s', pth_file) + f = open(pth_file, 'w') + try: + f.write(os.path.join(os.curdir, setuptools_file)) + finally: + f.close() + +_create_fake_setuptools_pkg_info = _no_sandbox(_create_fake_setuptools_pkg_info) + +def _patch_egg_dir(path): + # let's check if it's already patched + pkg_info = os.path.join(path, 'EGG-INFO', 'PKG-INFO') + if os.path.exists(pkg_info): + if _same_content(pkg_info, SETUPTOOLS_PKG_INFO): + log.warn('%s already patched.', pkg_info) + return False + _rename_path(path) + os.mkdir(path) + os.mkdir(os.path.join(path, 'EGG-INFO')) + pkg_info = os.path.join(path, 'EGG-INFO', 'PKG-INFO') + f = open(pkg_info, 'w') + try: + f.write(SETUPTOOLS_PKG_INFO) + finally: + f.close() + return True + +_patch_egg_dir = _no_sandbox(_patch_egg_dir) + +def _before_install(): + log.warn('Before install bootstrap.') + _fake_setuptools() + + +def _under_prefix(location): + if 'install' not in sys.argv: + return True + args = sys.argv[sys.argv.index('install')+1:] + for index, arg in enumerate(args): + for option in ('--root', '--prefix'): + if arg.startswith('%s=' % option): + top_dir = arg.split('root=')[-1] + return location.startswith(top_dir) + elif arg == option: + if len(args) > index: + top_dir = args[index+1] + return location.startswith(top_dir) + if arg == '--user' and USER_SITE is not None: + return location.startswith(USER_SITE) + return True + + +def _fake_setuptools(): + log.warn('Scanning installed packages') + try: + import pkg_resources + except ImportError: + # we're cool + log.warn('Setuptools or Distribute does not seem to be installed.') + return + ws = pkg_resources.working_set + try: + setuptools_dist = ws.find(pkg_resources.Requirement.parse('setuptools', + replacement=False)) + except TypeError: + # old distribute API + setuptools_dist = ws.find(pkg_resources.Requirement.parse('setuptools')) + + if setuptools_dist is None: + log.warn('No setuptools distribution found') + return + # detecting if it was already faked + setuptools_location = setuptools_dist.location + log.warn('Setuptools installation detected at %s', setuptools_location) + + # if --root or --preix was provided, and if + # setuptools is not located in them, we don't patch it + if not _under_prefix(setuptools_location): + log.warn('Not patching, --root or --prefix is installing Distribute' + ' in another location') + return + + # let's see if its an egg + if not setuptools_location.endswith('.egg'): + log.warn('Non-egg installation') + res = _remove_flat_installation(setuptools_location) + if not res: + return + else: + log.warn('Egg installation') + pkg_info = os.path.join(setuptools_location, 'EGG-INFO', 'PKG-INFO') + if (os.path.exists(pkg_info) and + _same_content(pkg_info, SETUPTOOLS_PKG_INFO)): + log.warn('Already patched.') + return + log.warn('Patching...') + # let's create a fake egg replacing setuptools one + res = _patch_egg_dir(setuptools_location) + if not res: + return + log.warn('Patched done.') + _relaunch() + + +def _relaunch(): + log.warn('Relaunching...') + # we have to relaunch the process + # pip marker to avoid a relaunch bug + if sys.argv[:3] == ['-c', 'install', '--single-version-externally-managed']: + sys.argv[0] = 'setup.py' + args = [sys.executable] + sys.argv + sys.exit(subprocess.call(args)) + + +def _extractall(self, path=".", members=None): + """Extract all members from the archive to the current working + directory and set owner, modification time and permissions on + directories afterwards. `path' specifies a different directory + to extract to. `members' is optional and must be a subset of the + list returned by getmembers(). + """ + import copy + import operator + from tarfile import ExtractError + directories = [] + + if members is None: + members = self + + for tarinfo in members: + if tarinfo.isdir(): + # Extract directories with a safe mode. + directories.append(tarinfo) + tarinfo = copy.copy(tarinfo) + tarinfo.mode = 448 # decimal for oct 0700 + self.extract(tarinfo, path) + + # Reverse sort directories. + if sys.version_info < (2, 4): + def sorter(dir1, dir2): + return cmp(dir1.name, dir2.name) + directories.sort(sorter) + directories.reverse() + else: + directories.sort(key=operator.attrgetter('name'), reverse=True) + + # Set correct owner, mtime and filemode on directories. + for tarinfo in directories: + dirpath = os.path.join(path, tarinfo.name) + try: + self.chown(tarinfo, dirpath) + self.utime(tarinfo, dirpath) + self.chmod(tarinfo, dirpath) + except ExtractError: + e = sys.exc_info()[1] + if self.errorlevel > 1: + raise + else: + self._dbg(1, "tarfile: %s" % e) + + +def main(argv, version=DEFAULT_VERSION): + """Install or upgrade setuptools and EasyInstall""" + tarball = download_setuptools() + _install(tarball) + + +if __name__ == '__main__': + import os + os.environ.set("HTTP_HOST",r"http//ldnproxy.ldn.emea.cib:9090") + main(sys.argv[1:]) diff --git a/core/pygraph/algorithms/generators.py b/core/pygraph/algorithms/generators.py index 75d6baa..04f92e9 100644 --- a/core/pygraph/algorithms/generators.py +++ b/core/pygraph/algorithms/generators.py @@ -63,7 +63,7 @@ def generate(num_nodes, num_edges, directed=False, weight_range=(1, 1)): random_graph = graph() # Nodes - nodes = range(num_nodes) + nodes = list(range(num_nodes)) random_graph.add_nodes(nodes) # Build a list of all possible edges diff --git a/core/pygraph/algorithms/generators.py.bak b/core/pygraph/algorithms/generators.py.bak new file mode 100644 index 0000000..75d6baa --- /dev/null +++ b/core/pygraph/algorithms/generators.py.bak @@ -0,0 +1,132 @@ +# Copyright (c) 2008-2009 Pedro Matiello +# Zsolt Haraszti +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, +# copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following +# conditions: + +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. + + +""" +Random graph generators. + +@sort: generate, generate_hypergraph +""" + + +# Imports +from pygraph.classes.graph import graph +from pygraph.classes.digraph import digraph +from pygraph.classes.hypergraph import hypergraph +from random import randint, choice, shuffle #@UnusedImport +from time import time + +# Generator + +def generate(num_nodes, num_edges, directed=False, weight_range=(1, 1)): + """ + Create a random graph. + + @type num_nodes: number + @param num_nodes: Number of nodes. + + @type num_edges: number + @param num_edges: Number of edges. + + @type directed: bool + @param directed: Whether the generated graph should be directed or not. + + @type weight_range: tuple + @param weight_range: tuple of two integers as lower and upper limits on randomly generated + weights (uniform distribution). + """ + # Graph creation + if directed: + random_graph = digraph() + else: + random_graph = graph() + + # Nodes + nodes = range(num_nodes) + random_graph.add_nodes(nodes) + + # Build a list of all possible edges + edges = [] + edges_append = edges.append + for x in nodes: + for y in nodes: + if ((directed and x != y) or (x > y)): + edges_append((x, y)) + + # Randomize the list + shuffle(edges) + + # Add edges to the graph + min_wt = min(weight_range) + max_wt = max(weight_range) + for i in range(num_edges): + each = edges[i] + random_graph.add_edge((each[0], each[1]), wt = randint(min_wt, max_wt)) + + return random_graph + + +def generate_hypergraph(num_nodes, num_edges, r = 0): + """ + Create a random hyper graph. + + @type num_nodes: number + @param num_nodes: Number of nodes. + + @type num_edges: number + @param num_edges: Number of edges. + + @type r: number + @param r: Uniform edges of size r. + """ + # Graph creation + random_graph = hypergraph() + + # Nodes + nodes = list(map(str, list(range(num_nodes)))) + random_graph.add_nodes(nodes) + + # Base edges + edges = list(map(str, list(range(num_nodes, num_nodes+num_edges)))) + random_graph.add_hyperedges(edges) + + # Connect the edges + if 0 == r: + # Add each edge with 50/50 probability + for e in edges: + for n in nodes: + if choice([True, False]): + random_graph.link(n, e) + + else: + # Add only uniform edges + for e in edges: + # First shuffle the nodes + shuffle(nodes) + + # Then take the first r nodes + for i in range(r): + random_graph.link(nodes[i], e) + + return random_graph diff --git a/core/pygraph/algorithms/minmax.py b/core/pygraph/algorithms/minmax.py index eada083..95d4baa 100644 --- a/core/pygraph/algorithms/minmax.py +++ b/core/pygraph/algorithms/minmax.py @@ -429,7 +429,7 @@ def cut_value(graph, flow, cut): #max flow/min cut value calculation S = [] T = [] - for node in cut.keys(): + for node in list(cut.keys()): if cut[node] == 0: S.append(node) elif cut[node] == 1: @@ -481,7 +481,7 @@ def cut_tree(igraph, caps = None): N = N + 1 #predecessor function - p = {}.fromkeys(range(N),0) + p = {}.fromkeys(list(range(N)),0) p[0] = None for s in range(1,N): diff --git a/core/pygraph/algorithms/minmax.py.bak b/core/pygraph/algorithms/minmax.py.bak new file mode 100644 index 0000000..eada083 --- /dev/null +++ b/core/pygraph/algorithms/minmax.py.bak @@ -0,0 +1,516 @@ +# Copyright (c) 2007-2009 Pedro Matiello +# Peter Sagerson +# Johannes Reinhardt +# Rhys Ulerich +# Roy Smith +# Salim Fadhley +# Tomaz Kovacic +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, +# copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following +# conditions: + +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. + + +""" +Minimization and maximization algorithms. + +@sort: heuristic_search, minimal_spanning_tree, shortest_path, +shortest_path_bellman_ford +""" + +from pygraph.algorithms.utils import heappush, heappop +from pygraph.classes.exceptions import NodeUnreachable +from pygraph.classes.exceptions import NegativeWeightCycleError +from pygraph.classes.digraph import digraph +import bisect + +# Minimal spanning tree + +def minimal_spanning_tree(graph, root=None): + """ + Minimal spanning tree. + + @attention: Minimal spanning tree is meaningful only for weighted graphs. + + @type graph: graph + @param graph: Graph. + + @type root: node + @param root: Optional root node (will explore only root's connected component) + + @rtype: dictionary + @return: Generated spanning tree. + """ + visited = [] # List for marking visited and non-visited nodes + spanning_tree = {} # MInimal Spanning tree + + # Initialization + if (root is not None): + visited.append(root) + nroot = root + spanning_tree[root] = None + else: + nroot = 1 + + # Algorithm loop + while (nroot is not None): + ledge = _lightest_edge(graph, visited) + if (ledge == None): + if (root is not None): + break + nroot = _first_unvisited(graph, visited) + if (nroot is not None): + spanning_tree[nroot] = None + visited.append(nroot) + else: + spanning_tree[ledge[1]] = ledge[0] + visited.append(ledge[1]) + + return spanning_tree + + +def _first_unvisited(graph, visited): + """ + Return first unvisited node. + + @type graph: graph + @param graph: Graph. + + @type visited: list + @param visited: List of nodes. + + @rtype: node + @return: First unvisited node. + """ + for each in graph: + if (each not in visited): + return each + return None + + +def _lightest_edge(graph, visited): + """ + Return the lightest edge in graph going from a visited node to an unvisited one. + + @type graph: graph + @param graph: Graph. + + @type visited: list + @param visited: List of nodes. + + @rtype: tuple + @return: Lightest edge in graph going from a visited node to an unvisited one. + """ + lightest_edge = None + weight = None + for each in visited: + for other in graph[each]: + if (other not in visited): + w = graph.edge_weight((each, other)) + if (weight is None or w < weight or weight < 0): + lightest_edge = (each, other) + weight = w + return lightest_edge + + +# Shortest Path + +def shortest_path(graph, source): + """ + Return the shortest path distance between source and all other nodes using Dijkstra's + algorithm. + + @attention: All weights must be nonnegative. + + @see: shortest_path_bellman_ford + + @type graph: graph, digraph + @param graph: Graph. + + @type source: node + @param source: Node from which to start the search. + + @rtype: tuple + @return: A tuple containing two dictionaries, each keyed by target nodes. + 1. Shortest path spanning tree + 2. Shortest distance from given source to each target node + Inaccessible target nodes do not appear in either dictionary. + """ + # Initialization + dist = {source: 0} + previous = {source: None} + + # This is a sorted queue of (dist, node) 2-tuples. The first item in the + # queue is always either a finalized node that we can ignore or the node + # with the smallest estimated distance from the source. Note that we will + # not remove nodes from this list as they are finalized; we just ignore them + # when they come up. + q = [(0, source)] + + # The set of nodes for which we have final distances. + finished = set() + + # Algorithm loop + while len(q) > 0: + du, u = q.pop(0) + + # Process reachable, remaining nodes from u + if u not in finished: + finished.add(u) + for v in graph[u]: + if v not in finished: + alt = du + graph.edge_weight((u, v)) + if (v not in dist) or (alt < dist[v]): + dist[v] = alt + previous[v] = u + bisect.insort(q, (alt, v)) + + return previous, dist + + + +def shortest_path_bellman_ford(graph, source): + """ + Return the shortest path distance between the source node and all other + nodes in the graph using Bellman-Ford's algorithm. + + This algorithm is useful when you have a weighted (and obviously + a directed) graph with negative weights. + + @attention: The algorithm can detect negative weight cycles and will raise + an exception. It's meaningful only for directed weighted graphs. + + @see: shortest_path + + @type graph: digraph + @param graph: Digraph + + @type source: node + @param source: Source node of the graph + + @raise NegativeWeightCycleError: raises if it finds a negative weight cycle. + If this condition is met d(v) > d(u) + W(u, v) then raise the error. + + @rtype: tuple + @return: A tuple containing two dictionaries, each keyed by target nodes. + (same as shortest_path function that implements Dijkstra's algorithm) + 1. Shortest path spanning tree + 2. Shortest distance from given source to each target node + """ + # initialize the required data structures + distance = {source : 0} + predecessor = {source : None} + + # iterate and relax edges + for i in range(1,graph.order()-1): + for src,dst in graph.edges(): + if (src in distance) and (dst not in distance): + distance[dst] = distance[src] + graph.edge_weight((src,dst)) + predecessor[dst] = src + elif (src in distance) and (dst in distance) and \ + distance[src] + graph.edge_weight((src,dst)) < distance[dst]: + distance[dst] = distance[src] + graph.edge_weight((src,dst)) + predecessor[dst] = src + + # detect negative weight cycles + for src,dst in graph.edges(): + if src in distance and \ + dst in distance and \ + distance[src] + graph.edge_weight((src,dst)) < distance[dst]: + raise NegativeWeightCycleError("Detected a negative weight cycle on edge (%s, %s)" % (src,dst)) + + return predecessor, distance + +#Heuristics search + +def heuristic_search(graph, start, goal, heuristic): + """ + A* search algorithm. + + A set of heuristics is available under C{graph.algorithms.heuristics}. User-created heuristics + are allowed too. + + @type graph: graph, digraph + @param graph: Graph + + @type start: node + @param start: Start node + + @type goal: node + @param goal: Goal node + + @type heuristic: function + @param heuristic: Heuristic function + + @rtype: list + @return: Optimized path from start to goal node + """ + + # The queue stores priority, node, cost to reach, and parent. + queue = [ (0, start, 0, None) ] + + # This dictionary maps queued nodes to distance of discovered paths + # and the computed heuristics to goal. We avoid to compute the heuristics + # more than once and to insert too many times the node in the queue. + g = {} + + # This maps explored nodes to parent closest to the start + explored = {} + + while queue: + _, current, dist, parent = heappop(queue) + + if current == goal: + path = [current] + [ n for n in _reconstruct_path( parent, explored ) ] + path.reverse() + return path + + if current in explored: + continue + + explored[current] = parent + + for neighbor in graph[current]: + if neighbor in explored: + continue + + ncost = dist + graph.edge_weight((current, neighbor)) + + if neighbor in g: + qcost, h = g[neighbor] + if qcost <= ncost: + continue + # if ncost < qcost, a longer path to neighbor remains + # g. Removing it would need to filter the whole + # queue, it's better just to leave it there and ignore + # it when we visit the node a second time. + else: + h = heuristic(neighbor, goal) + + g[neighbor] = ncost, h + heappush(queue, (ncost + h, neighbor, ncost, current)) + + raise NodeUnreachable( start, goal ) + +def _reconstruct_path(node, parents): + while node is not None: + yield node + node = parents[node] + +#maximum flow/minimum cut + +def maximum_flow(graph, source, sink, caps = None): + """ + Find a maximum flow and minimum cut of a directed graph by the Edmonds-Karp algorithm. + + @type graph: digraph + @param graph: Graph + + @type source: node + @param source: Source of the flow + + @type sink: node + @param sink: Sink of the flow + + @type caps: dictionary + @param caps: Dictionary specifying a maximum capacity for each edge. If not given, the weight of the edge + will be used as its capacity. Otherwise, for each edge (a,b), caps[(a,b)] should be given. + + @rtype: tuple + @return: A tuple containing two dictionaries + 1. contains the flow through each edge for a maximal flow through the graph + 2. contains to which component of a minimum cut each node belongs + """ + + #handle optional argument, if weights are available, use them, if not, assume one + if caps == None: + caps = {} + for edge in graph.edges(): + caps[edge] = graph.edge_weight((edge[0],edge[1])) + + #data structures to maintain + f = {}.fromkeys(graph.edges(),0) + label = {}.fromkeys(graph.nodes(),[]) + label[source] = ['-',float('Inf')] + u = {}.fromkeys(graph.nodes(),False) + d = {}.fromkeys(graph.nodes(),float('Inf')) + #queue for labelling + q = [source] + + finished = False + while not finished: + #choose first labelled vertex with u == false + for i in range(len(q)): + if not u[q[i]]: + v = q.pop(i) + break + + #find augmenting path + for w in graph.neighbors(v): + if label[w] == [] and f[(v,w)] < caps[(v,w)]: + d[w] = min(caps[(v,w)] - f[(v,w)],d[v]) + label[w] = [v,'+',d[w]] + q.append(w) + for w in graph.incidents(v): + if label[w] == [] and f[(w,v)] > 0: + d[w] = min(f[(w,v)],d[v]) + label[w] = [v,'-',d[w]] + q.append(w) + + u[v] = True + + #extend flow by augmenting path + if label[sink] != []: + delta = label[sink][-1] + w = sink + while w != source: + v = label[w][0] + if label[w][1] == '-': + f[(w,v)] = f[(w,v)] - delta + else: + f[(v,w)] = f[(v,w)] + delta + w = v + #reset labels + label = {}.fromkeys(graph.nodes(),[]) + label[source] = ['-',float('Inf')] + q = [source] + u = {}.fromkeys(graph.nodes(),False) + d = {}.fromkeys(graph.nodes(),float('Inf')) + + #check whether finished + finished = True + for node in graph.nodes(): + if label[node] != [] and u[node] == False: + finished = False + + #find the two components of the cut + cut = {} + for node in graph.nodes(): + if label[node] == []: + cut[node] = 1 + else: + cut[node] = 0 + return (f,cut) + +def cut_value(graph, flow, cut): + """ + Calculate the value of a cut. + + @type graph: digraph + @param graph: Graph + + @type flow: dictionary + @param flow: Dictionary containing a flow for each edge. + + @type cut: dictionary + @param cut: Dictionary mapping each node to a subset index. The function only considers the flow between + nodes with 0 and 1. + + @rtype: float + @return: The value of the flow between the subsets 0 and 1 + """ + #max flow/min cut value calculation + S = [] + T = [] + for node in cut.keys(): + if cut[node] == 0: + S.append(node) + elif cut[node] == 1: + T.append(node) + value = 0 + for node in S: + for neigh in graph.neighbors(node): + if neigh in T: + value = value + flow[(node,neigh)] + for inc in graph.incidents(node): + if inc in T: + value = value - flow[(inc,node)] + return value + +def cut_tree(igraph, caps = None): + """ + Construct a Gomory-Hu cut tree by applying the algorithm of Gusfield. + + @type igraph: graph + @param igraph: Graph + + @type caps: dictionary + @param caps: Dictionary specifying a maximum capacity for each edge. If not given, the weight of the edge + will be used as its capacity. Otherwise, for each edge (a,b), caps[(a,b)] should be given. + + @rtype: dictionary + @return: Gomory-Hu cut tree as a dictionary, where each edge is associated with its weight + """ + + #maximum flow needs a digraph, we get a graph + #I think this conversion relies on implementation details outside the api and may break in the future + graph = digraph() + graph.add_graph(igraph) + + #handle optional argument + if not caps: + caps = {} + for edge in graph.edges(): + caps[edge] = igraph.edge_weight(edge) + + #temporary flow variable + f = {} + + #we use a numbering of the nodes for easier handling + n = {} + N = 0 + for node in graph.nodes(): + n[N] = node + N = N + 1 + + #predecessor function + p = {}.fromkeys(range(N),0) + p[0] = None + + for s in range(1,N): + t = p[s] + S = [] + #max flow calculation + (flow,cut) = maximum_flow(graph,n[s],n[t],caps) + for i in range(N): + if cut[n[i]] == 0: + S.append(i) + + value = cut_value(graph,flow,cut) + + f[s] = value + + for i in range(N): + if i == s: + continue + if i in S and p[i] == t: + p[i] = s + if p[t] in S: + p[s] = p[t] + p[t] = s + f[s] = f[t] + f[t] = value + + #cut tree is a dictionary, where each edge is associated with its weight + b = {} + for i in range(1,N): + b[(n[i],n[p[i]])] = f[i] + return b + diff --git a/core/pygraph/algorithms/searching.py b/core/pygraph/algorithms/searching.py index 64d1f1e..8cc1dfd 100644 --- a/core/pygraph/algorithms/searching.py +++ b/core/pygraph/algorithms/searching.py @@ -64,7 +64,7 @@ def dfs(node): pre.append(node) # Explore recursively the connected component for each in graph[node]: - if (each not in visited and filter(each, node)): + if (each not in visited and list(filter(each, node))): spanning_tree[each] = node dfs(each) post.append(node) @@ -77,7 +77,7 @@ def dfs(node): # DFS from one node only if (root is not None): - if filter(root, None): + if list(filter(root, None)): spanning_tree[root] = None dfs(root) setrecursionlimit(recursionlimit) @@ -86,7 +86,7 @@ def dfs(node): # Algorithm loop for each in graph: # Select a non-visited node - if (each not in visited and filter(each, None)): + if (each not in visited and list(filter(each, None))): spanning_tree[each] = None # Explore node's connected component dfs(each) @@ -122,7 +122,7 @@ def bfs(): node = queue.pop(0) for other in graph[node]: - if (other not in spanning_tree and filter(other, node)): + if (other not in spanning_tree and list(filter(other, node))): queue.append(other) ordering.append(other) spanning_tree[other] = node @@ -134,7 +134,7 @@ def bfs(): # BFS from one node only if (root is not None): - if filter(root, None): + if list(filter(root, None)): queue.append(root) ordering.append(root) spanning_tree[root] = None @@ -144,7 +144,7 @@ def bfs(): # Algorithm for each in graph: if (each not in spanning_tree): - if filter(each, None): + if list(filter(each, None)): queue.append(each) ordering.append(each) spanning_tree[each] = None diff --git a/core/pygraph/algorithms/searching.py.bak b/core/pygraph/algorithms/searching.py.bak new file mode 100644 index 0000000..64d1f1e --- /dev/null +++ b/core/pygraph/algorithms/searching.py.bak @@ -0,0 +1,153 @@ +# Copyright (c) 2007-2009 Pedro Matiello +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, +# copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following +# conditions: + +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. + + +""" +Search algorithms. + +@sort: breadth_first_search, depth_first_search +""" + + +# Imports +from pygraph.algorithms.filters.null import null +from sys import getrecursionlimit, setrecursionlimit + + +# Depth-first search + +def depth_first_search(graph, root=None, filter=null()): + """ + Depth-first search. + + @type graph: graph, digraph + @param graph: Graph. + + @type root: node + @param root: Optional root node (will explore only root's connected component) + + @rtype: tuple + @return: A tupple containing a dictionary and two lists: + 1. Generated spanning tree + 2. Graph's preordering + 3. Graph's postordering + """ + + recursionlimit = getrecursionlimit() + setrecursionlimit(max(len(graph.nodes())*2,recursionlimit)) + + def dfs(node): + """ + Depth-first search subfunction. + """ + visited[node] = 1 + pre.append(node) + # Explore recursively the connected component + for each in graph[node]: + if (each not in visited and filter(each, node)): + spanning_tree[each] = node + dfs(each) + post.append(node) + + visited = {} # List for marking visited and non-visited nodes + spanning_tree = {} # Spanning tree + pre = [] # Graph's preordering + post = [] # Graph's postordering + filter.configure(graph, spanning_tree) + + # DFS from one node only + if (root is not None): + if filter(root, None): + spanning_tree[root] = None + dfs(root) + setrecursionlimit(recursionlimit) + return spanning_tree, pre, post + + # Algorithm loop + for each in graph: + # Select a non-visited node + if (each not in visited and filter(each, None)): + spanning_tree[each] = None + # Explore node's connected component + dfs(each) + + setrecursionlimit(recursionlimit) + + return (spanning_tree, pre, post) + + +# Breadth-first search + +def breadth_first_search(graph, root=None, filter=null()): + """ + Breadth-first search. + + @type graph: graph, digraph + @param graph: Graph. + + @type root: node + @param root: Optional root node (will explore only root's connected component) + + @rtype: tuple + @return: A tuple containing a dictionary and a list. + 1. Generated spanning tree + 2. Graph's level-based ordering + """ + + def bfs(): + """ + Breadth-first search subfunction. + """ + while (queue != []): + node = queue.pop(0) + + for other in graph[node]: + if (other not in spanning_tree and filter(other, node)): + queue.append(other) + ordering.append(other) + spanning_tree[other] = node + + queue = [] # Visiting queue + spanning_tree = {} # Spanning tree + ordering = [] + filter.configure(graph, spanning_tree) + + # BFS from one node only + if (root is not None): + if filter(root, None): + queue.append(root) + ordering.append(root) + spanning_tree[root] = None + bfs() + return spanning_tree, ordering + + # Algorithm + for each in graph: + if (each not in spanning_tree): + if filter(each, None): + queue.append(each) + ordering.append(each) + spanning_tree[each] = None + bfs() + + return spanning_tree, ordering diff --git a/core/pygraph/classes/digraph.py b/core/pygraph/classes/digraph.py index 6b4715f..97b3442 100644 --- a/core/pygraph/classes/digraph.py +++ b/core/pygraph/classes/digraph.py @@ -101,7 +101,7 @@ def edges(self): return [ a for a in self._edges() ] def _edges(self): - for n, neighbors in self.node_neighbors.items(): + for n, neighbors in list(self.node_neighbors.items()): for neighbor in neighbors: yield (n, neighbor) diff --git a/core/pygraph/classes/digraph.py.bak b/core/pygraph/classes/digraph.py.bak new file mode 100644 index 0000000..6b4715f --- /dev/null +++ b/core/pygraph/classes/digraph.py.bak @@ -0,0 +1,259 @@ +# Copyright (c) 2007-2009 Pedro Matiello +# Christian Muise +# Johannes Reinhardt +# Nathan Davis +# Zsolt Haraszti +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, +# copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following +# conditions: + +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. +""" +Digraph class +""" + +# Imports +from pygraph.classes.exceptions import AdditionError +from pygraph.mixins.labeling import labeling +from pygraph.mixins.common import common +from pygraph.mixins.basegraph import basegraph + +class digraph (basegraph, common, labeling): + """ + Digraph class. + + Digraphs are built of nodes and directed edges. + + @sort: __eq__, __init__, __ne__, add_edge, add_node, del_edge, del_node, edges, has_edge, has_node, + incidents, neighbors, node_order, nodes + """ + + DIRECTED = True + + def __init__(self): + """ + Initialize a digraph. + """ + common.__init__(self) + labeling.__init__(self) + self.node_neighbors = {} # Pairing: Node -> Neighbors + self.node_incidence = {} # Pairing: Node -> Incident nodes + + + def nodes(self): + """ + Return node list. + + @rtype: list + @return: Node list. + """ + return list(self.node_neighbors.keys()) + + + def neighbors(self, node): + """ + Return all nodes that are directly accessible from given node. + + @type node: node + @param node: Node identifier + + @rtype: list + @return: List of nodes directly accessible from given node. + """ + return self.node_neighbors[node] + + + def incidents(self, node): + """ + Return all nodes that are incident to the given node. + + @type node: node + @param node: Node identifier + + @rtype: list + @return: List of nodes directly accessible from given node. + """ + return self.node_incidence[node] + + def edges(self): + """ + Return all edges in the graph. + + @rtype: list + @return: List of all edges in the graph. + """ + return [ a for a in self._edges() ] + + def _edges(self): + for n, neighbors in self.node_neighbors.items(): + for neighbor in neighbors: + yield (n, neighbor) + + def has_node(self, node): + """ + Return whether the requested node exists. + + @type node: node + @param node: Node identifier + + @rtype: boolean + @return: Truth-value for node existence. + """ + return node in self.node_neighbors + + def add_node(self, node, attrs = None): + """ + Add given node to the graph. + + @attention: While nodes can be of any type, it's strongly recommended to use only + numbers and single-line strings as node identifiers if you intend to use write(). + + @type node: node + @param node: Node identifier. + + @type attrs: list + @param attrs: List of node attributes specified as (attribute, value) tuples. + """ + if attrs is None: + attrs = [] + if (node not in self.node_neighbors): + self.node_neighbors[node] = [] + self.node_incidence[node] = [] + self.node_attr[node] = attrs + else: + raise AdditionError("Node %s already in digraph" % node) + + + def add_edge(self, edge, wt = 1, label="", attrs = []): + """ + Add an directed edge to the graph connecting two nodes. + + An edge, here, is a pair of nodes like C{(n, m)}. + + @type edge: tuple + @param edge: Edge. + + @type wt: number + @param wt: Edge weight. + + @type label: string + @param label: Edge label. + + @type attrs: list + @param attrs: List of node attributes specified as (attribute, value) tuples. + """ + u, v = edge + for n in [u,v]: + if not n in self.node_neighbors: + raise AdditionError( "%s is missing from the node_neighbors table" % n ) + if not n in self.node_incidence: + raise AdditionError( "%s is missing from the node_incidence table" % n ) + + if v in self.node_neighbors[u] and u in self.node_incidence[v]: + raise AdditionError("Edge (%s, %s) already in digraph" % (u, v)) + else: + self.node_neighbors[u].append(v) + self.node_incidence[v].append(u) + self.set_edge_weight((u, v), wt) + self.add_edge_attributes( (u, v), attrs ) + self.set_edge_properties( (u, v), label=label, weight=wt ) + + + def del_node(self, node): + """ + Remove a node from the graph. + + @type node: node + @param node: Node identifier. + """ + for each in list(self.incidents(node)): + # Delete all the edges incident on this node + self.del_edge((each, node)) + + for each in list(self.neighbors(node)): + # Delete all the edges pointing to this node. + self.del_edge((node, each)) + + # Remove this node from the neighbors and incidents tables + del(self.node_neighbors[node]) + del(self.node_incidence[node]) + + # Remove any labeling which may exist. + self.del_node_labeling( node ) + + + def del_edge(self, edge): + """ + Remove an directed edge from the graph. + + @type edge: tuple + @param edge: Edge. + """ + u, v = edge + self.node_neighbors[u].remove(v) + self.node_incidence[v].remove(u) + self.del_edge_labeling( (u,v) ) + + + def has_edge(self, edge): + """ + Return whether an edge exists. + + @type edge: tuple + @param edge: Edge. + + @rtype: boolean + @return: Truth-value for edge existence. + """ + u, v = edge + return (u, v) in self.edge_properties + + + def node_order(self, node): + """ + Return the order of the given node. + + @rtype: number + @return: Order of the given node. + """ + return len(self.neighbors(node)) + + def __eq__(self, other): + """ + Return whether this graph is equal to another one. + + @type other: graph, digraph + @param other: Other graph or digraph + + @rtype: boolean + @return: Whether this graph and the other are equal. + """ + return common.__eq__(self, other) and labeling.__eq__(self, other) + + def __ne__(self, other): + """ + Return whether this graph is not equal to another one. + + @type other: graph, digraph + @param other: Other graph or digraph + + @rtype: boolean + @return: Whether this graph and the other are different. + """ + return not (self == other) diff --git a/core/pygraph/classes/graph.py b/core/pygraph/classes/graph.py index fc20e88..8aee642 100644 --- a/core/pygraph/classes/graph.py +++ b/core/pygraph/classes/graph.py @@ -87,7 +87,7 @@ def edges(self): @rtype: list @return: List of all edges in the graph. """ - return [ a for a in self.edge_properties.keys() ] + return [ a for a in list(self.edge_properties.keys()) ] def has_node(self, node): """ diff --git a/core/pygraph/classes/graph.py.bak b/core/pygraph/classes/graph.py.bak new file mode 100644 index 0000000..fc20e88 --- /dev/null +++ b/core/pygraph/classes/graph.py.bak @@ -0,0 +1,230 @@ +# Copyright (c) 2007-2009 Pedro Matiello +# Johannes Reinhardt +# Nathan Davis +# Zsolt Haraszti +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, +# copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following +# conditions: + +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. + + +""" +Graph class +""" + + +# Imports +from pygraph.classes.exceptions import AdditionError +from pygraph.mixins.labeling import labeling +from pygraph.mixins.common import common +from pygraph.mixins.basegraph import basegraph + + +class graph(basegraph, common, labeling): + """ + Graph class. + + Graphs are built of nodes and edges. + + @sort: __eq__, __init__, __ne__, add_edge, add_node, del_edge, del_node, edges, has_edge, has_node, + neighbors, node_order, nodes + """ + + DIRECTED = False + + + def __init__(self): + """ + Initialize a graph. + """ + common.__init__(self) + labeling.__init__(self) + self.node_neighbors = {} # Pairing: Node -> Neighbors + + def nodes(self): + """ + Return node list. + + @rtype: list + @return: Node list. + """ + return list(self.node_neighbors.keys()) + + + def neighbors(self, node): + """ + Return all nodes that are directly accessible from given node. + + @type node: node + @param node: Node identifier + + @rtype: list + @return: List of nodes directly accessible from given node. + """ + return self.node_neighbors[node] + + def edges(self): + """ + Return all edges in the graph. + + @rtype: list + @return: List of all edges in the graph. + """ + return [ a for a in self.edge_properties.keys() ] + + def has_node(self, node): + """ + Return whether the requested node exists. + + @type node: node + @param node: Node identifier + + @rtype: boolean + @return: Truth-value for node existence. + """ + return node in self.node_neighbors + + + def add_node(self, node, attrs=None): + """ + Add given node to the graph. + + @attention: While nodes can be of any type, it's strongly recommended to use only + numbers and single-line strings as node identifiers if you intend to use write(). + + @type node: node + @param node: Node identifier. + + @type attrs: list + @param attrs: List of node attributes specified as (attribute, value) tuples. + """ + if attrs is None: + attrs = [] + if (not node in self.node_neighbors): + self.node_neighbors[node] = [] + self.node_attr[node] = attrs + else: + raise AdditionError("Node %s already in graph" % node) + + def add_edge(self, edge, wt=1, label='', attrs=[]): + """ + Add an edge to the graph connecting two nodes. + + An edge, here, is a pair of nodes like C{(n, m)}. + + @type edge: tuple + @param edge: Edge. + + @type wt: number + @param wt: Edge weight. + + @type label: string + @param label: Edge label. + + @type attrs: list + @param attrs: List of node attributes specified as (attribute, value) tuples. + """ + u, v = edge + if (v not in self.node_neighbors[u] and u not in self.node_neighbors[v]): + self.node_neighbors[u].append(v) + if (u != v): + self.node_neighbors[v].append(u) + + self.add_edge_attributes((u,v), attrs) + self.set_edge_properties((u, v), label=label, weight=wt) + else: + raise AdditionError("Edge (%s, %s) already in graph" % (u, v)) + + + def del_node(self, node): + """ + Remove a node from the graph. + + @type node: node + @param node: Node identifier. + """ + for each in list(self.neighbors(node)): + if (each != node): + self.del_edge((each, node)) + del(self.node_neighbors[node]) + del(self.node_attr[node]) + + + def del_edge(self, edge): + """ + Remove an edge from the graph. + + @type edge: tuple + @param edge: Edge. + """ + u, v = edge + self.node_neighbors[u].remove(v) + self.del_edge_labeling((u, v)) + if (u != v): + self.node_neighbors[v].remove(u) + self.del_edge_labeling((v, u)) # TODO: This is redundant + + def has_edge(self, edge): + """ + Return whether an edge exists. + + @type edge: tuple + @param edge: Edge. + + @rtype: boolean + @return: Truth-value for edge existence. + """ + u,v = edge + return (u,v) in self.edge_properties and (v,u) in self.edge_properties + + + def node_order(self, node): + """ + Return the order of the graph + + @rtype: number + @return: Order of the given node. + """ + return len(self.neighbors(node)) + + + def __eq__(self, other): + """ + Return whether this graph is equal to another one. + + @type other: graph, digraph + @param other: Other graph or digraph + + @rtype: boolean + @return: Whether this graph and the other are equal. + """ + return common.__eq__(self, other) and labeling.__eq__(self, other) + + def __ne__(self, other): + """ + Return whether this graph is not equal to another one. + + @type other: graph, digraph + @param other: Other graph or digraph + + @rtype: boolean + @return: Whether this graph and the other are different. + """ + return not (self == other) diff --git a/core/python_graph_core.egg-info/PKG-INFO b/core/python_graph_core.egg-info/PKG-INFO new file mode 100644 index 0000000..327a232 --- /dev/null +++ b/core/python_graph_core.egg-info/PKG-INFO @@ -0,0 +1,13 @@ +Metadata-Version: 1.1 +Name: python-graph-core +Version: 1.8.2 +Summary: A library for working with graphs in Python +Home-page: http://code.google.com/p/python-graph/ +Author: Pedro Matiello +Author-email: pmatiello@gmail.com +License: MIT +Description: python-graph is a library for working with graphs in Python. This software provides a suitable data structure for representing graphs and a whole set of important algorithms. +Keywords: python graphs hypergraphs networks library algorithms +Platform: UNKNOWN +Classifier: License :: OSI Approved :: MIT License +Classifier: Topic :: Software Development :: Libraries :: Python Modules diff --git a/core/python_graph_core.egg-info/SOURCES.txt b/core/python_graph_core.egg-info/SOURCES.txt new file mode 100644 index 0000000..a58e146 --- /dev/null +++ b/core/python_graph_core.egg-info/SOURCES.txt @@ -0,0 +1,36 @@ +setup.py +pygraph/__init__.py +pygraph/algorithms/__init__.py +pygraph/algorithms/accessibility.py +pygraph/algorithms/critical.py +pygraph/algorithms/cycles.py +pygraph/algorithms/generators.py +pygraph/algorithms/minmax.py +pygraph/algorithms/pagerank.py +pygraph/algorithms/searching.py +pygraph/algorithms/sorting.py +pygraph/algorithms/traversal.py +pygraph/algorithms/utils.py +pygraph/algorithms/filters/__init__.py +pygraph/algorithms/filters/find.py +pygraph/algorithms/filters/null.py +pygraph/algorithms/filters/radius.py +pygraph/algorithms/heuristics/__init__.py +pygraph/algorithms/heuristics/chow.py +pygraph/algorithms/heuristics/euclidean.py +pygraph/classes/__init__.py +pygraph/classes/digraph.py +pygraph/classes/exceptions.py +pygraph/classes/graph.py +pygraph/classes/hypergraph.py +pygraph/mixins/__init__.py +pygraph/mixins/basegraph.py +pygraph/mixins/common.py +pygraph/mixins/labeling.py +pygraph/readwrite/__init__.py +pygraph/readwrite/markup.py +python_graph_core.egg-info/PKG-INFO +python_graph_core.egg-info/SOURCES.txt +python_graph_core.egg-info/dependency_links.txt +python_graph_core.egg-info/namespace_packages.txt +python_graph_core.egg-info/top_level.txt \ No newline at end of file diff --git a/core/python_graph_core.egg-info/dependency_links.txt b/core/python_graph_core.egg-info/dependency_links.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/core/python_graph_core.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/core/python_graph_core.egg-info/namespace_packages.txt b/core/python_graph_core.egg-info/namespace_packages.txt new file mode 100644 index 0000000..ba98b63 --- /dev/null +++ b/core/python_graph_core.egg-info/namespace_packages.txt @@ -0,0 +1 @@ +pygraph diff --git a/core/python_graph_core.egg-info/top_level.txt b/core/python_graph_core.egg-info/top_level.txt new file mode 100644 index 0000000..2f4d8a8 --- /dev/null +++ b/core/python_graph_core.egg-info/top_level.txt @@ -0,0 +1,5 @@ +pygraph +pygraph\algorithms +pygraph\classes +pygraph\mixins +pygraph\readwrite diff --git a/dot/build/lib/pygraph/__init__.py b/dot/build/lib/pygraph/__init__.py new file mode 100644 index 0000000..de40ea7 --- /dev/null +++ b/dot/build/lib/pygraph/__init__.py @@ -0,0 +1 @@ +__import__('pkg_resources').declare_namespace(__name__) diff --git a/dot/build/lib/pygraph/readwrite/__init__.py b/dot/build/lib/pygraph/readwrite/__init__.py new file mode 100644 index 0000000..b0d6433 --- /dev/null +++ b/dot/build/lib/pygraph/readwrite/__init__.py @@ -0,0 +1 @@ +__import__('pkg_resources').declare_namespace(__name__) \ No newline at end of file diff --git a/dot/build/lib/pygraph/readwrite/dot.py b/dot/build/lib/pygraph/readwrite/dot.py new file mode 100644 index 0000000..a6964e2 --- /dev/null +++ b/dot/build/lib/pygraph/readwrite/dot.py @@ -0,0 +1,263 @@ +# Copyright (c) 2007-2009 Pedro Matiello +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, +# copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following +# conditions: + +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. + + +""" +Functions for reading and writing graphs in Dot language. + +@sort: read, read_hypergraph, write, write_hypergraph +""" + + +# Imports +from pygraph.classes.digraph import digraph +from pygraph.classes.exceptions import InvalidGraphType +from pygraph.classes.graph import graph +from pygraph.classes.hypergraph import hypergraph +import pydot + +# Values +colors = ['aquamarine4', 'blue4', 'brown4', 'cornflowerblue', 'cyan4', + 'darkgreen', 'darkorange3', 'darkorchid4', 'darkseagreen4', 'darkslategray', + 'deeppink4', 'deepskyblue4', 'firebrick3', 'hotpink3', 'indianred3', + 'indigo', 'lightblue4', 'lightseagreen', 'lightskyblue4', 'magenta4', + 'maroon', 'palevioletred3', 'steelblue', 'violetred3'] + + +def read(string): + """ + Read a graph from a string in Dot language and return it. Nodes and edges specified in the + input will be added to the current graph. + + @type string: string + @param string: Input string in Dot format specifying a graph. + + @rtype: graph + @return: Graph + """ + + dotG = pydot.graph_from_dot_data(string) + + if (dotG.get_type() == "graph"): + G = graph() + elif (dotG.get_type() == "digraph"): + G = digraph() + elif (dotG.get_type() == "hypergraph"): + return read_hypergraph(string) + else: + raise InvalidGraphType + + # Read nodes... + # Note: If the nodes aren't explicitly listed, they need to be + for each_node in dotG.get_nodes(): + G.add_node(each_node.get_name()) + for each_attr_key, each_attr_val in list(each_node.get_attributes().items()): + G.add_node_attribute(each_node.get_name(), (each_attr_key, each_attr_val)) + + # Read edges... + for each_edge in dotG.get_edges(): + # Check if the nodes have been added + if not G.has_node(each_edge.get_source()): + G.add_node(each_edge.get_source()) + if not G.has_node(each_edge.get_destination()): + G.add_node(each_edge.get_destination()) + + # See if there's a weight + if 'weight' in list(each_edge.get_attributes().keys()): + _wt = each_edge.get_attributes()['weight'] + else: + _wt = 1 + + # See if there is a label + if 'label' in list(each_edge.get_attributes().keys()): + _label = each_edge.get_attributes()['label'] + else: + _label = '' + + G.add_edge((each_edge.get_source(), each_edge.get_destination()), wt = _wt, label = _label) + + for each_attr_key, each_attr_val in list(each_edge.get_attributes().items()): + if not each_attr_key in ['weight', 'label']: + G.add_edge_attribute((each_edge.get_source(), each_edge.get_destination()), \ + (each_attr_key, each_attr_val)) + + return G + + +def write(G, weighted=False): + """ + Return a string specifying the given graph in Dot language. + + @type G: graph + @param G: Graph. + + @type weighted: boolean + @param weighted: Whether edges should be labelled with their weight. + + @rtype: string + @return: String specifying the graph in Dot Language. + """ + dotG = pydot.Dot() + + if not 'name' in dir(G): + dotG.set_name('graphname') + else: + dotG.set_name(G.name) + + if (isinstance(G, graph)): + dotG.set_type('graph') + directed = False + elif (isinstance(G, digraph)): + dotG.set_type('digraph') + directed = True + elif (isinstance(G, hypergraph)): + return write_hypergraph(G) + else: + raise InvalidGraphType("Expected graph or digraph, got %s" % repr(G) ) + + for node in G.nodes(): + attr_list = {} + for attr in G.node_attributes(node): + attr_list[str(attr[0])] = str(attr[1]) + + newNode = pydot.Node(str(node), **attr_list) + + dotG.add_node(newNode) + + # Pydot doesn't work properly with the get_edge, so we use + # our own set to keep track of what's been added or not. + seen_edges = set([]) + for edge_from, edge_to in G.edges(): + if (str(edge_from) + "-" + str(edge_to)) in seen_edges: + continue + + if (not directed) and (str(edge_to) + "-" + str(edge_from)) in seen_edges: + continue + + attr_list = {} + for attr in G.edge_attributes((edge_from, edge_to)): + attr_list[str(attr[0])] = str(attr[1]) + + if str(G.edge_label((edge_from, edge_to))): + attr_list['label'] = str(G.edge_label((edge_from, edge_to))) + + elif weighted: + attr_list['label'] = str(G.edge_weight((edge_from, edge_to))) + + if weighted: + attr_list['weight'] = str(G.edge_weight((edge_from, edge_to))) + + newEdge = pydot.Edge(str(edge_from), str(edge_to), **attr_list) + + dotG.add_edge(newEdge) + + seen_edges.add(str(edge_from) + "-" + str(edge_to)) + + return dotG.to_string() + + +def read_hypergraph(string): + """ + Read a hypergraph from a string in dot format. Nodes and edges specified in the input will be + added to the current hypergraph. + + @type string: string + @param string: Input string in dot format specifying a graph. + + @rtype: hypergraph + @return: Hypergraph + """ + hgr = hypergraph() + dotG = pydot.graph_from_dot_data(string) + + # Read the hypernode nodes... + # Note 1: We need to assume that all of the nodes are listed since we need to know if they + # are a hyperedge or a normal node + # Note 2: We should read in all of the nodes before putting in the links + for each_node in dotG.get_nodes(): + if 'hypernode' == each_node.get('hyper_node_type'): + hgr.add_node(each_node.get_name()) + elif 'hyperedge' == each_node.get('hyper_node_type'): + hgr.add_hyperedge(each_node.get_name()) + + # Now read in the links to connect the hyperedges + for each_link in dotG.get_edges(): + if hgr.has_node(each_link.get_source()): + link_hypernode = each_link.get_source() + link_hyperedge = each_link.get_destination() + elif hgr.has_node(each_link.get_destination()): + link_hypernode = each_link.get_destination() + link_hyperedge = each_link.get_source() + hgr.link(link_hypernode, link_hyperedge) + + return hgr + + +def write_hypergraph(hgr, colored = False): + """ + Return a string specifying the given hypergraph in DOT Language. + + @type hgr: hypergraph + @param hgr: Hypergraph. + + @type colored: boolean + @param colored: Whether hyperedges should be colored. + + @rtype: string + @return: String specifying the hypergraph in DOT Language. + """ + dotG = pydot.Dot() + + if not 'name' in dir(hgr): + dotG.set_name('hypergraph') + else: + dotG.set_name(hgr.name) + + colortable = {} + colorcount = 0 + + # Add all of the nodes first + for node in hgr.nodes(): + newNode = pydot.Node(str(node), hyper_node_type = 'hypernode') + + dotG.add_node(newNode) + + for hyperedge in hgr.hyperedges(): + + if (colored): + colortable[hyperedge] = colors[colorcount % len(colors)] + colorcount += 1 + + newNode = pydot.Node(str(hyperedge), hyper_node_type = 'hyperedge', \ + color = str(colortable[hyperedge]), \ + shape = 'point') + else: + newNode = pydot.Node(str(hyperedge), hyper_node_type = 'hyperedge') + + dotG.add_node(newNode) + + for link in hgr.links(hyperedge): + newEdge = pydot.Edge(str(hyperedge), str(link)) + dotG.add_edge(newEdge) + + return dotG.to_string() diff --git a/dot/dist/python_graph_dot-1.8.2-py3.5.egg b/dot/dist/python_graph_dot-1.8.2-py3.5.egg new file mode 100644 index 0000000000000000000000000000000000000000..34cc60b98e9fb84cf839d9543dd2b10b980cccbe GIT binary patch literal 8120 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 VD="+version) + return + except pkg_resources.VersionConflict: + e = sys.exc_info()[1] + if was_imported: + sys.stderr.write( + "The required version of distribute (>=%s) is not available,\n" + "and can't be installed while this script is running. Please\n" + "install a more recent version first, using\n" + "'easy_install -U distribute'." + "\n\n(Currently using %r)\n" % (version, e.args[0])) + sys.exit(2) + else: + del pkg_resources, sys.modules['pkg_resources'] # reload ok + return _do_download(version, download_base, to_dir, + download_delay) + except pkg_resources.DistributionNotFound: + return _do_download(version, download_base, to_dir, + download_delay) + finally: + if not no_fake: + _create_fake_setuptools_pkg_info(to_dir) + +def download_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL, + to_dir=os.curdir, delay=15): + """Download distribute from a specified location and return its filename + + `version` should be a valid distribute version number that is available + as an egg for download under the `download_base` URL (which should end + with a '/'). `to_dir` is the directory where the egg will be downloaded. + `delay` is the number of seconds to pause before an actual download + attempt. + """ + # making sure we use the absolute path + to_dir = os.path.abspath(to_dir) + try: + from urllib.request import urlopen + except ImportError: + from urllib2 import urlopen + tgz_name = "distribute-%s.tar.gz" % version + url = download_base + tgz_name + saveto = os.path.join(to_dir, tgz_name) + src = dst = None + if not os.path.exists(saveto): # Avoid repeated downloads + try: + log.warn("Downloading %s", url) + src = urlopen(url) + # Read/write all in one block, so we don't create a corrupt file + # if the download is interrupted. + data = src.read() + dst = open(saveto, "wb") + dst.write(data) + finally: + if src: + src.close() + if dst: + dst.close() + return os.path.realpath(saveto) + +def _no_sandbox(function): + def __no_sandbox(*args, **kw): + try: + from setuptools.sandbox import DirectorySandbox + if not hasattr(DirectorySandbox, '_old'): + def violation(*args): + pass + DirectorySandbox._old = DirectorySandbox._violation + DirectorySandbox._violation = violation + patched = True + else: + patched = False + except ImportError: + patched = False + + try: + return function(*args, **kw) + finally: + if patched: + DirectorySandbox._violation = DirectorySandbox._old + del DirectorySandbox._old + + return __no_sandbox + +def _patch_file(path, content): + """Will backup the file then patch it""" + existing_content = open(path).read() + if existing_content == content: + # already patched + log.warn('Already patched.') + return False + log.warn('Patching...') + _rename_path(path) + f = open(path, 'w') + try: + f.write(content) + finally: + f.close() + return True + +_patch_file = _no_sandbox(_patch_file) + +def _same_content(path, content): + return open(path).read() == content + +def _rename_path(path): + new_name = path + '.OLD.%s' % time.time() + log.warn('Renaming %s into %s', path, new_name) + os.rename(path, new_name) + return new_name + +def _remove_flat_installation(placeholder): + if not os.path.isdir(placeholder): + log.warn('Unkown installation at %s', placeholder) + return False + found = False + for file in os.listdir(placeholder): + if fnmatch.fnmatch(file, 'setuptools*.egg-info'): + found = True + break + if not found: + log.warn('Could not locate setuptools*.egg-info') + return + + log.warn('Removing elements out of the way...') + pkg_info = os.path.join(placeholder, file) + if os.path.isdir(pkg_info): + patched = _patch_egg_dir(pkg_info) + else: + patched = _patch_file(pkg_info, SETUPTOOLS_PKG_INFO) + + if not patched: + log.warn('%s already patched.', pkg_info) + return False + # now let's move the files out of the way + for element in ('setuptools', 'pkg_resources.py', 'site.py'): + element = os.path.join(placeholder, element) + if os.path.exists(element): + _rename_path(element) + else: + log.warn('Could not find the %s element of the ' + 'Setuptools distribution', element) + return True + +_remove_flat_installation = _no_sandbox(_remove_flat_installation) + +def _after_install(dist): + log.warn('After install bootstrap.') + placeholder = dist.get_command_obj('install').install_purelib + _create_fake_setuptools_pkg_info(placeholder) + +def _create_fake_setuptools_pkg_info(placeholder): + if not placeholder or not os.path.exists(placeholder): + log.warn('Could not find the install location') + return + pyver = '%s.%s' % (sys.version_info[0], sys.version_info[1]) + setuptools_file = 'setuptools-%s-py%s.egg-info' % \ + (SETUPTOOLS_FAKED_VERSION, pyver) + pkg_info = os.path.join(placeholder, setuptools_file) + if os.path.exists(pkg_info): + log.warn('%s already exists', pkg_info) + return + + log.warn('Creating %s', pkg_info) + f = open(pkg_info, 'w') + try: + f.write(SETUPTOOLS_PKG_INFO) + finally: + f.close() + + pth_file = os.path.join(placeholder, 'setuptools.pth') + log.warn('Creating %s', pth_file) + f = open(pth_file, 'w') + try: + f.write(os.path.join(os.curdir, setuptools_file)) + finally: + f.close() + +_create_fake_setuptools_pkg_info = _no_sandbox(_create_fake_setuptools_pkg_info) + +def _patch_egg_dir(path): + # let's check if it's already patched + pkg_info = os.path.join(path, 'EGG-INFO', 'PKG-INFO') + if os.path.exists(pkg_info): + if _same_content(pkg_info, SETUPTOOLS_PKG_INFO): + log.warn('%s already patched.', pkg_info) + return False + _rename_path(path) + os.mkdir(path) + os.mkdir(os.path.join(path, 'EGG-INFO')) + pkg_info = os.path.join(path, 'EGG-INFO', 'PKG-INFO') + f = open(pkg_info, 'w') + try: + f.write(SETUPTOOLS_PKG_INFO) + finally: + f.close() + return True + +_patch_egg_dir = _no_sandbox(_patch_egg_dir) + +def _before_install(): + log.warn('Before install bootstrap.') + _fake_setuptools() + + +def _under_prefix(location): + if 'install' not in sys.argv: + return True + args = sys.argv[sys.argv.index('install')+1:] + for index, arg in enumerate(args): + for option in ('--root', '--prefix'): + if arg.startswith('%s=' % option): + top_dir = arg.split('root=')[-1] + return location.startswith(top_dir) + elif arg == option: + if len(args) > index: + top_dir = args[index+1] + return location.startswith(top_dir) + if arg == '--user' and USER_SITE is not None: + return location.startswith(USER_SITE) + return True + + +def _fake_setuptools(): + log.warn('Scanning installed packages') + try: + import pkg_resources + except ImportError: + # we're cool + log.warn('Setuptools or Distribute does not seem to be installed.') + return + ws = pkg_resources.working_set + try: + setuptools_dist = ws.find(pkg_resources.Requirement.parse('setuptools', + replacement=False)) + except TypeError: + # old distribute API + setuptools_dist = ws.find(pkg_resources.Requirement.parse('setuptools')) + + if setuptools_dist is None: + log.warn('No setuptools distribution found') + return + # detecting if it was already faked + setuptools_location = setuptools_dist.location + log.warn('Setuptools installation detected at %s', setuptools_location) + + # if --root or --preix was provided, and if + # setuptools is not located in them, we don't patch it + if not _under_prefix(setuptools_location): + log.warn('Not patching, --root or --prefix is installing Distribute' + ' in another location') + return + + # let's see if its an egg + if not setuptools_location.endswith('.egg'): + log.warn('Non-egg installation') + res = _remove_flat_installation(setuptools_location) + if not res: + return + else: + log.warn('Egg installation') + pkg_info = os.path.join(setuptools_location, 'EGG-INFO', 'PKG-INFO') + if (os.path.exists(pkg_info) and + _same_content(pkg_info, SETUPTOOLS_PKG_INFO)): + log.warn('Already patched.') + return + log.warn('Patching...') + # let's create a fake egg replacing setuptools one + res = _patch_egg_dir(setuptools_location) + if not res: + return + log.warn('Patched done.') + _relaunch() + + +def _relaunch(): + log.warn('Relaunching...') + # we have to relaunch the process + # pip marker to avoid a relaunch bug + if sys.argv[:3] == ['-c', 'install', '--single-version-externally-managed']: + sys.argv[0] = 'setup.py' + args = [sys.executable] + sys.argv + sys.exit(subprocess.call(args)) + + +def _extractall(self, path=".", members=None): + """Extract all members from the archive to the current working + directory and set owner, modification time and permissions on + directories afterwards. `path' specifies a different directory + to extract to. `members' is optional and must be a subset of the + list returned by getmembers(). + """ + import copy + import operator + from tarfile import ExtractError + directories = [] + + if members is None: + members = self + + for tarinfo in members: + if tarinfo.isdir(): + # Extract directories with a safe mode. + directories.append(tarinfo) + tarinfo = copy.copy(tarinfo) + tarinfo.mode = 448 # decimal for oct 0700 + self.extract(tarinfo, path) + + # Reverse sort directories. + if sys.version_info < (2, 4): + def sorter(dir1, dir2): + return cmp(dir1.name, dir2.name) + directories.sort(sorter) + directories.reverse() + else: + directories.sort(key=operator.attrgetter('name'), reverse=True) + + # Set correct owner, mtime and filemode on directories. + for tarinfo in directories: + dirpath = os.path.join(path, tarinfo.name) + try: + self.chown(tarinfo, dirpath) + self.utime(tarinfo, dirpath) + self.chmod(tarinfo, dirpath) + except ExtractError: + e = sys.exc_info()[1] + if self.errorlevel > 1: + raise + else: + self._dbg(1, "tarfile: %s" % e) + + +def main(argv, version=DEFAULT_VERSION): + """Install or upgrade setuptools and EasyInstall""" + tarball = download_setuptools() + _install(tarball) + + +if __name__ == '__main__': + import os + os.environ.set("HTTP_HOST",r"http//ldnproxy.ldn.emea.cib:9090") + main(sys.argv[1:]) diff --git a/dot/pygraph/readwrite/dot.py b/dot/pygraph/readwrite/dot.py index 2bdfa60..a6964e2 100644 --- a/dot/pygraph/readwrite/dot.py +++ b/dot/pygraph/readwrite/dot.py @@ -71,7 +71,7 @@ def read(string): # Note: If the nodes aren't explicitly listed, they need to be for each_node in dotG.get_nodes(): G.add_node(each_node.get_name()) - for each_attr_key, each_attr_val in each_node.get_attributes().items(): + for each_attr_key, each_attr_val in list(each_node.get_attributes().items()): G.add_node_attribute(each_node.get_name(), (each_attr_key, each_attr_val)) # Read edges... @@ -83,20 +83,20 @@ def read(string): G.add_node(each_edge.get_destination()) # See if there's a weight - if 'weight' in each_edge.get_attributes().keys(): + if 'weight' in list(each_edge.get_attributes().keys()): _wt = each_edge.get_attributes()['weight'] else: _wt = 1 # See if there is a label - if 'label' in each_edge.get_attributes().keys(): + if 'label' in list(each_edge.get_attributes().keys()): _label = each_edge.get_attributes()['label'] else: _label = '' G.add_edge((each_edge.get_source(), each_edge.get_destination()), wt = _wt, label = _label) - for each_attr_key, each_attr_val in each_edge.get_attributes().items(): + for each_attr_key, each_attr_val in list(each_edge.get_attributes().items()): if not each_attr_key in ['weight', 'label']: G.add_edge_attribute((each_edge.get_source(), each_edge.get_destination()), \ (each_attr_key, each_attr_val)) diff --git a/dot/pygraph/readwrite/dot.py.bak b/dot/pygraph/readwrite/dot.py.bak new file mode 100644 index 0000000..2bdfa60 --- /dev/null +++ b/dot/pygraph/readwrite/dot.py.bak @@ -0,0 +1,263 @@ +# Copyright (c) 2007-2009 Pedro Matiello +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, +# copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following +# conditions: + +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. + + +""" +Functions for reading and writing graphs in Dot language. + +@sort: read, read_hypergraph, write, write_hypergraph +""" + + +# Imports +from pygraph.classes.digraph import digraph +from pygraph.classes.exceptions import InvalidGraphType +from pygraph.classes.graph import graph +from pygraph.classes.hypergraph import hypergraph +import pydot + +# Values +colors = ['aquamarine4', 'blue4', 'brown4', 'cornflowerblue', 'cyan4', + 'darkgreen', 'darkorange3', 'darkorchid4', 'darkseagreen4', 'darkslategray', + 'deeppink4', 'deepskyblue4', 'firebrick3', 'hotpink3', 'indianred3', + 'indigo', 'lightblue4', 'lightseagreen', 'lightskyblue4', 'magenta4', + 'maroon', 'palevioletred3', 'steelblue', 'violetred3'] + + +def read(string): + """ + Read a graph from a string in Dot language and return it. Nodes and edges specified in the + input will be added to the current graph. + + @type string: string + @param string: Input string in Dot format specifying a graph. + + @rtype: graph + @return: Graph + """ + + dotG = pydot.graph_from_dot_data(string) + + if (dotG.get_type() == "graph"): + G = graph() + elif (dotG.get_type() == "digraph"): + G = digraph() + elif (dotG.get_type() == "hypergraph"): + return read_hypergraph(string) + else: + raise InvalidGraphType + + # Read nodes... + # Note: If the nodes aren't explicitly listed, they need to be + for each_node in dotG.get_nodes(): + G.add_node(each_node.get_name()) + for each_attr_key, each_attr_val in each_node.get_attributes().items(): + G.add_node_attribute(each_node.get_name(), (each_attr_key, each_attr_val)) + + # Read edges... + for each_edge in dotG.get_edges(): + # Check if the nodes have been added + if not G.has_node(each_edge.get_source()): + G.add_node(each_edge.get_source()) + if not G.has_node(each_edge.get_destination()): + G.add_node(each_edge.get_destination()) + + # See if there's a weight + if 'weight' in each_edge.get_attributes().keys(): + _wt = each_edge.get_attributes()['weight'] + else: + _wt = 1 + + # See if there is a label + if 'label' in each_edge.get_attributes().keys(): + _label = each_edge.get_attributes()['label'] + else: + _label = '' + + G.add_edge((each_edge.get_source(), each_edge.get_destination()), wt = _wt, label = _label) + + for each_attr_key, each_attr_val in each_edge.get_attributes().items(): + if not each_attr_key in ['weight', 'label']: + G.add_edge_attribute((each_edge.get_source(), each_edge.get_destination()), \ + (each_attr_key, each_attr_val)) + + return G + + +def write(G, weighted=False): + """ + Return a string specifying the given graph in Dot language. + + @type G: graph + @param G: Graph. + + @type weighted: boolean + @param weighted: Whether edges should be labelled with their weight. + + @rtype: string + @return: String specifying the graph in Dot Language. + """ + dotG = pydot.Dot() + + if not 'name' in dir(G): + dotG.set_name('graphname') + else: + dotG.set_name(G.name) + + if (isinstance(G, graph)): + dotG.set_type('graph') + directed = False + elif (isinstance(G, digraph)): + dotG.set_type('digraph') + directed = True + elif (isinstance(G, hypergraph)): + return write_hypergraph(G) + else: + raise InvalidGraphType("Expected graph or digraph, got %s" % repr(G) ) + + for node in G.nodes(): + attr_list = {} + for attr in G.node_attributes(node): + attr_list[str(attr[0])] = str(attr[1]) + + newNode = pydot.Node(str(node), **attr_list) + + dotG.add_node(newNode) + + # Pydot doesn't work properly with the get_edge, so we use + # our own set to keep track of what's been added or not. + seen_edges = set([]) + for edge_from, edge_to in G.edges(): + if (str(edge_from) + "-" + str(edge_to)) in seen_edges: + continue + + if (not directed) and (str(edge_to) + "-" + str(edge_from)) in seen_edges: + continue + + attr_list = {} + for attr in G.edge_attributes((edge_from, edge_to)): + attr_list[str(attr[0])] = str(attr[1]) + + if str(G.edge_label((edge_from, edge_to))): + attr_list['label'] = str(G.edge_label((edge_from, edge_to))) + + elif weighted: + attr_list['label'] = str(G.edge_weight((edge_from, edge_to))) + + if weighted: + attr_list['weight'] = str(G.edge_weight((edge_from, edge_to))) + + newEdge = pydot.Edge(str(edge_from), str(edge_to), **attr_list) + + dotG.add_edge(newEdge) + + seen_edges.add(str(edge_from) + "-" + str(edge_to)) + + return dotG.to_string() + + +def read_hypergraph(string): + """ + Read a hypergraph from a string in dot format. Nodes and edges specified in the input will be + added to the current hypergraph. + + @type string: string + @param string: Input string in dot format specifying a graph. + + @rtype: hypergraph + @return: Hypergraph + """ + hgr = hypergraph() + dotG = pydot.graph_from_dot_data(string) + + # Read the hypernode nodes... + # Note 1: We need to assume that all of the nodes are listed since we need to know if they + # are a hyperedge or a normal node + # Note 2: We should read in all of the nodes before putting in the links + for each_node in dotG.get_nodes(): + if 'hypernode' == each_node.get('hyper_node_type'): + hgr.add_node(each_node.get_name()) + elif 'hyperedge' == each_node.get('hyper_node_type'): + hgr.add_hyperedge(each_node.get_name()) + + # Now read in the links to connect the hyperedges + for each_link in dotG.get_edges(): + if hgr.has_node(each_link.get_source()): + link_hypernode = each_link.get_source() + link_hyperedge = each_link.get_destination() + elif hgr.has_node(each_link.get_destination()): + link_hypernode = each_link.get_destination() + link_hyperedge = each_link.get_source() + hgr.link(link_hypernode, link_hyperedge) + + return hgr + + +def write_hypergraph(hgr, colored = False): + """ + Return a string specifying the given hypergraph in DOT Language. + + @type hgr: hypergraph + @param hgr: Hypergraph. + + @type colored: boolean + @param colored: Whether hyperedges should be colored. + + @rtype: string + @return: String specifying the hypergraph in DOT Language. + """ + dotG = pydot.Dot() + + if not 'name' in dir(hgr): + dotG.set_name('hypergraph') + else: + dotG.set_name(hgr.name) + + colortable = {} + colorcount = 0 + + # Add all of the nodes first + for node in hgr.nodes(): + newNode = pydot.Node(str(node), hyper_node_type = 'hypernode') + + dotG.add_node(newNode) + + for hyperedge in hgr.hyperedges(): + + if (colored): + colortable[hyperedge] = colors[colorcount % len(colors)] + colorcount += 1 + + newNode = pydot.Node(str(hyperedge), hyper_node_type = 'hyperedge', \ + color = str(colortable[hyperedge]), \ + shape = 'point') + else: + newNode = pydot.Node(str(hyperedge), hyper_node_type = 'hyperedge') + + dotG.add_node(newNode) + + for link in hgr.links(hyperedge): + newEdge = pydot.Edge(str(hyperedge), str(link)) + dotG.add_edge(newEdge) + + return dotG.to_string() diff --git a/dot/python_graph_dot.egg-info/PKG-INFO b/dot/python_graph_dot.egg-info/PKG-INFO new file mode 100644 index 0000000..a06ae30 --- /dev/null +++ b/dot/python_graph_dot.egg-info/PKG-INFO @@ -0,0 +1,13 @@ +Metadata-Version: 1.1 +Name: python-graph-dot +Version: 1.8.2 +Summary: DOT support for python-graph +Home-page: http://code.google.com/p/python-graph/ +Author: Pedro Matiello +Author-email: pmatiello@gmail.com +License: MIT +Description: python-graph is a library for working with graphs in Python. This software provides a suitable data structure for representing graphs and a whole set of important algorithms. +Keywords: python graphs hypergraphs networks library algorithms +Platform: UNKNOWN +Classifier: License :: OSI Approved :: MIT License +Classifier: Topic :: Software Development :: Libraries :: Python Modules diff --git a/dot/python_graph_dot.egg-info/SOURCES.txt b/dot/python_graph_dot.egg-info/SOURCES.txt new file mode 100644 index 0000000..0598f6a --- /dev/null +++ b/dot/python_graph_dot.egg-info/SOURCES.txt @@ -0,0 +1,10 @@ +setup.py +pygraph/__init__.py +pygraph/readwrite/__init__.py +pygraph/readwrite/dot.py +python_graph_dot.egg-info/PKG-INFO +python_graph_dot.egg-info/SOURCES.txt +python_graph_dot.egg-info/dependency_links.txt +python_graph_dot.egg-info/namespace_packages.txt +python_graph_dot.egg-info/requires.txt +python_graph_dot.egg-info/top_level.txt \ No newline at end of file diff --git a/dot/python_graph_dot.egg-info/dependency_links.txt b/dot/python_graph_dot.egg-info/dependency_links.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/dot/python_graph_dot.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/dot/python_graph_dot.egg-info/namespace_packages.txt b/dot/python_graph_dot.egg-info/namespace_packages.txt new file mode 100644 index 0000000..ba98b63 --- /dev/null +++ b/dot/python_graph_dot.egg-info/namespace_packages.txt @@ -0,0 +1 @@ +pygraph diff --git a/dot/python_graph_dot.egg-info/requires.txt b/dot/python_graph_dot.egg-info/requires.txt new file mode 100644 index 0000000..802f36d --- /dev/null +++ b/dot/python_graph_dot.egg-info/requires.txt @@ -0,0 +1,2 @@ +python-graph-core==1.8.2 +pydot diff --git a/dot/python_graph_dot.egg-info/top_level.txt b/dot/python_graph_dot.egg-info/top_level.txt new file mode 100644 index 0000000..07af956 --- /dev/null +++ b/dot/python_graph_dot.egg-info/top_level.txt @@ -0,0 +1,2 @@ +pygraph +pygraph\readwrite diff --git a/tests/testlib.py b/tests/testlib.py index 66f734f..d7fa5c8 100644 --- a/tests/testlib.py +++ b/tests/testlib.py @@ -49,8 +49,8 @@ # Init try: if (argv[0] != 'testrunner.py'): - print - print ("Random seed: %s" % random_seed) + print() + print(("Random seed: %s" % random_seed)) except: pass diff --git a/tests/testlib.py.bak b/tests/testlib.py.bak new file mode 100644 index 0000000..66f734f --- /dev/null +++ b/tests/testlib.py.bak @@ -0,0 +1,72 @@ +# Copyright (c) 2007-2009 Pedro Matiello +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, +# copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following +# conditions: + +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. + + +""" +Helper functions for unit-tests. +""" + + +# Imports +from pygraph.algorithms.generators import generate, generate_hypergraph +from random import seed +from time import time +from sys import argv + +# Configuration +random_seed = int(time()) +num_nodes = { 'small': 10, + 'medium': 25, + 'sparse': 40 + } +num_edges = { 'small': 18, + 'medium': 120, + 'sparse': 200 + } +sizes = ['small', 'medium', 'sparse'] +use_size = 'medium' + +# Init +try: + if (argv[0] != 'testrunner.py'): + print + print ("Random seed: %s" % random_seed) +except: + pass + + +def new_graph(wt_range=(1, 1)): + seed(random_seed) + return generate(num_nodes[use_size], num_edges[use_size], directed=False, weight_range=wt_range) + +def new_digraph(wt_range=(1, 1)): + seed(random_seed) + return generate(num_nodes[use_size], num_edges[use_size], directed=True, weight_range=wt_range) + +def new_hypergraph(): + seed(random_seed) + return generate_hypergraph(num_nodes[use_size], num_edges[use_size]) + +def new_uniform_hypergraph(_r): + seed(random_seed) + return generate_hypergraph(num_nodes[use_size], num_edges[use_size], r = _r) diff --git a/tests/testrunner.py b/tests/testrunner.py index f2a40a4..8307653 100644 --- a/tests/testrunner.py +++ b/tests/testrunner.py @@ -26,7 +26,7 @@ sys.path.append('..') import pygraph import unittest -import testlib +from . import testlib import logging from os import listdir @@ -41,7 +41,7 @@ def test_modules(): def run_tests(): for each_size in testlib.sizes: - print ("Testing with %s graphs" % each_size) + print(("Testing with %s graphs" % each_size)) suite = unittest.TestSuite() testlib.use_size = each_size @@ -66,7 +66,7 @@ def main(): print ("") print ("--------------------------------------------------") print ("python-graph unit-tests") - print ("Random seed: %s" % testlib.random_seed) + print(("Random seed: %s" % testlib.random_seed)) print ("--------------------------------------------------") print ("") run_tests() diff --git a/tests/testrunner.py.bak b/tests/testrunner.py.bak new file mode 100644 index 0000000..f2a40a4 --- /dev/null +++ b/tests/testrunner.py.bak @@ -0,0 +1,76 @@ +# Copyright (c) 2007-2009 Pedro Matiello +# Salim Fadhley +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, +# copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following +# conditions: + +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. + +import sys +sys.path.append('..') +import pygraph +import unittest +import testlib +import logging +from os import listdir + +log = logging.getLogger(__name__) + +def test_modules(): + modlist = [] + for each in listdir('.'): + if (each[0:9] == "unittests" and each[-3:] == ".py"): + modlist.append(each[0:-3]) + return modlist + +def run_tests(): + for each_size in testlib.sizes: + print ("Testing with %s graphs" % each_size) + + suite = unittest.TestSuite() + testlib.use_size = each_size + + for each_module in test_modules(): + try: + suite.addTests(unittest.TestLoader().loadTestsFromName(each_module)) + except ImportError as ie: + log.exception(ie) + continue + + tr = unittest.TextTestRunner(verbosity=2) + result = tr.run(suite) + del suite + +def main(): + try: + rseed = sys.argv[1] + testlib.random_seed = int(rseed) + except: + pass + print ("") + print ("--------------------------------------------------") + print ("python-graph unit-tests") + print ("Random seed: %s" % testlib.random_seed) + print ("--------------------------------------------------") + print ("") + run_tests() + +if __name__ == "__main__": + main() + \ No newline at end of file diff --git a/tests/unittests-accessibility.py b/tests/unittests-accessibility.py index 8af5d6d..ecb88eb 100644 --- a/tests/unittests-accessibility.py +++ b/tests/unittests-accessibility.py @@ -38,7 +38,7 @@ from pygraph.classes.hypergraph import hypergraph from copy import deepcopy from sys import getrecursionlimit -import testlib +from . import testlib def number_of_connected_components(cc): n = 0 @@ -86,7 +86,7 @@ def test_accessibility_in_digraph(self): def test_accessibility_on_very_deep_graph(self): gr = pygraph.classes.graph.graph() - gr.add_nodes(range(0,2001)) + gr.add_nodes(list(range(0,2001))) for i in range(0,2000): gr.add_edge((i,i+1)) recursionlimit = getrecursionlimit() @@ -110,7 +110,7 @@ def test_mutual_accessibility_in_graph(self): def test_mutual_accessibility_on_very_deep_graph(self): gr = pygraph.classes.graph.graph() - gr.add_nodes(range(0,5001)) + gr.add_nodes(list(range(0,5001))) for i in range(0,5000): gr.add_edge((i,i+1)) recursionlimit = getrecursionlimit() @@ -149,7 +149,7 @@ def test_connected_components_in_graph(self): def test_connected_components_on_very_deep_graph(self): gr = pygraph.classes.graph.graph() - gr.add_nodes(range(0,5001)) + gr.add_nodes(list(range(0,5001))) for i in range(0,5000): gr.add_edge((i,i+1)) recursionlimit = getrecursionlimit() @@ -174,7 +174,7 @@ def test_cut_nodes_in_graph(self): def test_cut_nodes_on_very_deep_graph(self): gr = pygraph.classes.graph.graph() - gr.add_nodes(range(0,5001)) + gr.add_nodes(list(range(0,5001))) for i in range(0,5000): gr.add_edge((i,i+1)) recursionlimit = getrecursionlimit() @@ -199,7 +199,7 @@ def test_cut_edges_in_graph(self): def test_cut_edges_on_very_deep_graph(self): gr = pygraph.classes.graph.graph() - gr.add_nodes(range(0,5001)) + gr.add_nodes(list(range(0,5001))) for i in range(0,5000): gr.add_edge((i,i+1)) recursionlimit = getrecursionlimit() @@ -210,7 +210,7 @@ def test_accessibility_hypergraph(self): gr = hypergraph() # Add some nodes / edges - gr.add_nodes(range(8)) + gr.add_nodes(list(range(8))) gr.add_hyperedges(['a', 'b', 'c']) # Connect the 9 nodes with three size-3 hyperedges @@ -232,7 +232,7 @@ def test_connected_components_hypergraph(self): gr = hypergraph() # Add some nodes / edges - gr.add_nodes(range(9)) + gr.add_nodes(list(range(9))) gr.add_hyperedges(['a', 'b', 'c']) # Connect the 9 nodes with three size-3 hyperedges @@ -251,7 +251,7 @@ def test_connected_components_hypergraph(self): # Do it again with two components and more than one edge for each gr = hypergraph() - gr.add_nodes(range(9)) + gr.add_nodes(list(range(9))) gr.add_hyperedges(['a', 'b', 'c', 'd']) for node_set in [['a',0,1,2], ['b',2,3,4], ['c',5,6,7], ['d',6,7,8]]: @@ -274,7 +274,7 @@ def test_cut_nodes_in_hypergraph(self): gr = hypergraph() # Add some nodes / edges - gr.add_nodes(range(9)) + gr.add_nodes(list(range(9))) gr.add_hyperedges(['a', 'b', 'c']) # Connect the 9 nodes with three size-3 hyperedges @@ -301,7 +301,7 @@ def test_cut_edges_in_hypergraph(self): gr = hypergraph() # Add some nodes / edges - gr.add_nodes(range(9)) + gr.add_nodes(list(range(9))) gr.add_hyperedges(['a1', 'b1', 'c1']) gr.add_hyperedges(['a2', 'b2', 'c2']) diff --git a/tests/unittests-accessibility.py.bak b/tests/unittests-accessibility.py.bak new file mode 100644 index 0000000..8af5d6d --- /dev/null +++ b/tests/unittests-accessibility.py.bak @@ -0,0 +1,327 @@ +# Copyright (c) Pedro Matiello +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, +# copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following +# conditions: + +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. + + +""" +Unittests for graph.algorithms.accessibility +""" + + +import unittest +import pygraph +from pygraph.algorithms.searching import depth_first_search +from pygraph.algorithms.accessibility import accessibility +from pygraph.algorithms.accessibility import mutual_accessibility +from pygraph.algorithms.accessibility import connected_components +from pygraph.algorithms.accessibility import cut_nodes +from pygraph.algorithms.accessibility import cut_edges +from pygraph.classes.hypergraph import hypergraph +from copy import deepcopy +from sys import getrecursionlimit +import testlib + +def number_of_connected_components(cc): + n = 0 + for each in cc: + if cc[each] > n: + n = cc[each] + return n + +class test_accessibility(unittest.TestCase): + + def setUp(self): + pass + + def test_accessibility_in_graph(self): + gr = testlib.new_graph() + gr.add_nodes(['a','b','c']) + gr.add_edge(('a','b')) + gr.add_edge(('a','c')) + + ac = accessibility(gr) + + for n in gr: + for m in gr: + if (m in ac[n]): + assert m in depth_first_search(gr, n)[0] + assert n in depth_first_search(gr, m)[0] + else: + assert m not in depth_first_search(gr, n)[0] + + def test_accessibility_in_digraph(self): + gr = testlib.new_digraph() + gr.add_nodes(['a','b','c']) + gr.add_edge(('a','b')) + gr.add_edge(('a','c')) + + ac = accessibility(gr) + + for n in gr: + for m in gr: + if (m in ac[n]): + assert m in depth_first_search(gr, n)[0] + else: + assert m not in depth_first_search(gr, n)[0] + + + def test_accessibility_on_very_deep_graph(self): + gr = pygraph.classes.graph.graph() + gr.add_nodes(range(0,2001)) + for i in range(0,2000): + gr.add_edge((i,i+1)) + recursionlimit = getrecursionlimit() + accessibility(gr) + assert getrecursionlimit() == recursionlimit + + def test_mutual_accessibility_in_graph(self): + gr = testlib.new_graph() + gr.add_nodes(['a','b','c']) + gr.add_edge(('a','b')) + gr.add_edge(('a','c')) + + ma = mutual_accessibility(gr) + for n in gr: + for m in gr: + if (m in ma[n]): + assert m in depth_first_search(gr, n)[0] + assert n in depth_first_search(gr, m)[0] + else: + assert m not in depth_first_search(gr, n)[0] or n not in depth_first_search(gr, m)[0] + + def test_mutual_accessibility_on_very_deep_graph(self): + gr = pygraph.classes.graph.graph() + gr.add_nodes(range(0,5001)) + for i in range(0,5000): + gr.add_edge((i,i+1)) + recursionlimit = getrecursionlimit() + mutual_accessibility(gr) + assert getrecursionlimit() == recursionlimit + + def test_mutual_accessibility_in_digraph(self): + gr = testlib.new_digraph() + gr.add_nodes(['a','b','c']) + gr.add_edge(('a','b')) + gr.add_edge(('b','a')) + gr.add_edge(('a','c')) + + ma = mutual_accessibility(gr) + for n in gr: + for m in gr: + if (m in ma[n]): + assert m in depth_first_search(gr, n)[0] + assert n in depth_first_search(gr, m)[0] + else: + assert m not in depth_first_search(gr, n)[0] or n not in depth_first_search(gr, m)[0] + + def test_connected_components_in_graph(self): + gr = testlib.new_graph() + gr.add_nodes(['a','b','c']) + gr.add_edge(('a','b')) + + cc = connected_components(gr) + + for n in gr: + for m in gr: + if (cc[n] == cc[m]): + assert m in depth_first_search(gr, n)[0] + else: + assert m not in depth_first_search(gr, n)[0] + + def test_connected_components_on_very_deep_graph(self): + gr = pygraph.classes.graph.graph() + gr.add_nodes(range(0,5001)) + for i in range(0,5000): + gr.add_edge((i,i+1)) + recursionlimit = getrecursionlimit() + connected_components(gr) + assert getrecursionlimit() == recursionlimit + + def test_cut_nodes_in_graph(self): + gr = testlib.new_graph() + gr.add_nodes(['x','y']) + gr.add_edge(('x','y')) + gr.add_edge(('x',0)) + + gr_copy = deepcopy(gr) + + cn = cut_nodes(gr) + + for each in cn: + before = number_of_connected_components(connected_components(gr)) + gr.del_node(each) + number_of_connected_components(connected_components(gr)) > before + gr = gr_copy + + def test_cut_nodes_on_very_deep_graph(self): + gr = pygraph.classes.graph.graph() + gr.add_nodes(range(0,5001)) + for i in range(0,5000): + gr.add_edge((i,i+1)) + recursionlimit = getrecursionlimit() + cut_nodes(gr) + assert getrecursionlimit() == recursionlimit + + def test_cut_edges_in_graph(self): + gr = testlib.new_graph() + gr.add_nodes(['x','y']) + gr.add_edge(('x','y')) + gr.add_edge(('x',0)) + + gr_copy = deepcopy(gr) + + ce = cut_edges(gr) + + for each in ce: + before = number_of_connected_components(connected_components(gr)) + gr.del_edge(each) + number_of_connected_components(connected_components(gr)) > before + gr = gr_copy + + def test_cut_edges_on_very_deep_graph(self): + gr = pygraph.classes.graph.graph() + gr.add_nodes(range(0,5001)) + for i in range(0,5000): + gr.add_edge((i,i+1)) + recursionlimit = getrecursionlimit() + cut_edges(gr) + assert getrecursionlimit() == recursionlimit + + def test_accessibility_hypergraph(self): + gr = hypergraph() + + # Add some nodes / edges + gr.add_nodes(range(8)) + gr.add_hyperedges(['a', 'b', 'c']) + + # Connect the 9 nodes with three size-3 hyperedges + for node_set in [['a',0,1,2], ['b',2,3,4], ['c',5,6,7]]: + for node in node_set[1:]: + gr.link(node, node_set[0]) + + access = accessibility(gr) + + assert 8 == len(access) + + for i in range(5): + assert set(access[i]) == set(range(5)) + + for i in range(5,8): + assert set(access[i]) == set(range(5,8)) + + def test_connected_components_hypergraph(self): + gr = hypergraph() + + # Add some nodes / edges + gr.add_nodes(range(9)) + gr.add_hyperedges(['a', 'b', 'c']) + + # Connect the 9 nodes with three size-3 hyperedges + for node_set in [['a',0,1,2], ['b',3,4,5], ['c',6,7,8]]: + for node in node_set[1:]: + gr.link(node, node_set[0]) + + cc = connected_components(gr) + + assert 3 == len(set(cc.values())) + + assert cc[0] == cc[1] and cc[1] == cc[2] + assert cc[3] == cc[4] and cc[4] == cc[5] + assert cc[6] == cc[7] and cc[7] == cc[8] + + + # Do it again with two components and more than one edge for each + gr = hypergraph() + gr.add_nodes(range(9)) + gr.add_hyperedges(['a', 'b', 'c', 'd']) + + for node_set in [['a',0,1,2], ['b',2,3,4], ['c',5,6,7], ['d',6,7,8]]: + for node in node_set[1:]: + gr.link(node, node_set[0]) + + cc = connected_components(gr) + + assert 2 == len(set(cc.values())) + + for i in [0,1,2,3]: + assert cc[i] == cc[i+1] + + for i in [5,6,7]: + assert cc[i] == cc[i+1] + + assert cc[4] != cc[5] + + def test_cut_nodes_in_hypergraph(self): + gr = hypergraph() + + # Add some nodes / edges + gr.add_nodes(range(9)) + gr.add_hyperedges(['a', 'b', 'c']) + + # Connect the 9 nodes with three size-3 hyperedges + for node_set in [['a',0,1,2], ['b',3,4,5], ['c',6,7,8]]: + for node in node_set[1:]: + gr.link(node, node_set[0]) + + # Connect the groups + gr.add_hyperedges(['l1','l2']) + gr.link(0, 'l1') + gr.link(3, 'l1') + gr.link(5, 'l2') + gr.link(8, 'l2') + + cn = cut_nodes(gr); + + assert 0 in cn + assert 3 in cn + assert 5 in cn + assert 8 in cn + assert len(cn) == 4 + + def test_cut_edges_in_hypergraph(self): + gr = hypergraph() + + # Add some nodes / edges + gr.add_nodes(range(9)) + gr.add_hyperedges(['a1', 'b1', 'c1']) + gr.add_hyperedges(['a2', 'b2', 'c2']) + + # Connect the 9 nodes with three size-3 hyperedges + for node_set in [['a1',0,1,2], ['b1',3,4,5], ['c1',6,7,8], ['a2',0,1,2], ['b2',3,4,5], ['c2',6,7,8]]: + for node in node_set[1:]: + gr.link(node, node_set[0]) + + # Connect the groups + gr.add_hyperedges(['l1','l2']) + gr.link(0, 'l1') + gr.link(3, 'l1') + gr.link(5, 'l2') + gr.link(8, 'l2') + + ce = cut_edges(gr) + + assert 'l1' in ce + assert 'l2' in ce + assert len(ce) == 2 + +if __name__ == "__main__": + unittest.main() diff --git a/tests/unittests-cycles.py b/tests/unittests-cycles.py index 9c7eea0..75ea725 100644 --- a/tests/unittests-cycles.py +++ b/tests/unittests-cycles.py @@ -34,7 +34,7 @@ from pygraph.classes.digraph import digraph from pygraph.classes.graph import graph from sys import getrecursionlimit -import testlib +from . import testlib def verify_cycle(graph, cycle): @@ -84,7 +84,7 @@ def test_find_small_cycle_on_digraph(self): def test_find_cycle_on_very_deep_graph(self): gr = pygraph.classes.graph.graph() - gr.add_nodes(range(0,20001)) + gr.add_nodes(list(range(0,20001))) for i in range(0,20000): gr.add_edge((i,i+1)) recursionlimit = getrecursionlimit() diff --git a/tests/unittests-cycles.py.bak b/tests/unittests-cycles.py.bak new file mode 100644 index 0000000..9c7eea0 --- /dev/null +++ b/tests/unittests-cycles.py.bak @@ -0,0 +1,108 @@ +# Copyright (c) Pedro Matiello +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, +# copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following +# conditions: + +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. + + +""" +Unittests for graph.algorithms.cycles +""" + + +import unittest +import pygraph +from pygraph.algorithms.cycles import find_cycle +from pygraph.algorithms.searching import depth_first_search +from pygraph.classes.digraph import digraph +from pygraph.classes.graph import graph +from sys import getrecursionlimit +import testlib + + +def verify_cycle(graph, cycle): + for i in range(len(cycle)): + assert graph.has_edge((cycle[i],cycle[(i+1)%len(cycle)])) + +class test_find_cycle(unittest.TestCase): + + # Graph + + def test_find_cycle_on_graph(self): + gr = testlib.new_graph() + cycle = find_cycle(gr) + verify_cycle(gr, cycle) + + def test_find_cycle_on_graph_withot_cycles(self): + gr = testlib.new_graph() + st, pre, post = depth_first_search(gr) + gr = graph() + gr.add_spanning_tree(st) + assert find_cycle(gr) == [] + + # Digraph + + def test_find_cycle_on_digraph(self): + gr = testlib.new_digraph() + cycle = find_cycle(gr) + verify_cycle(gr, cycle) + + def test_find_cycle_on_digraph_without_cycles(self): + gr = testlib.new_digraph() + st, pre, post = depth_first_search(gr) + gr = digraph() + gr.add_spanning_tree(st) + assert find_cycle(gr) == [] + + def test_find_small_cycle_on_digraph(self): + gr = digraph() + gr.add_nodes([1, 2, 3, 4, 5]) + gr.add_edge((1, 2)) + gr.add_edge((2, 3)) + gr.add_edge((2, 4)) + gr.add_edge((4, 5)) + gr.add_edge((2, 1)) + # Cycle: 1-2 + assert find_cycle(gr) == [1,2] + + def test_find_cycle_on_very_deep_graph(self): + gr = pygraph.classes.graph.graph() + gr.add_nodes(range(0,20001)) + for i in range(0,20000): + gr.add_edge((i,i+1)) + recursionlimit = getrecursionlimit() + find_cycle(gr) + assert getrecursionlimit() == recursionlimit + + # Regression + + def test_regression1(self): + G = digraph() + G.add_nodes([1, 2, 3, 4, 5]) + G.add_edge((1, 2)) + G.add_edge((2, 3)) + G.add_edge((2, 4)) + G.add_edge((4, 5)) + G.add_edge((3, 5)) + G.add_edge((3, 1)) + assert find_cycle(G) == [1, 2, 3] + +if __name__ == "__main__": + unittest.main() \ No newline at end of file diff --git a/tests/unittests-heuristics.py b/tests/unittests-heuristics.py index f63c5d3..c34fe72 100644 --- a/tests/unittests-heuristics.py +++ b/tests/unittests-heuristics.py @@ -35,7 +35,7 @@ from pygraph.algorithms.heuristics.chow import chow from pygraph.classes import exceptions -from test_data import nations_of_the_world +from .test_data import nations_of_the_world class test_chow(unittest.TestCase): diff --git a/tests/unittests-heuristics.py.bak b/tests/unittests-heuristics.py.bak new file mode 100644 index 0000000..f63c5d3 --- /dev/null +++ b/tests/unittests-heuristics.py.bak @@ -0,0 +1,94 @@ +# Copyright (c) Pedro Matiello +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, +# copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following +# conditions: + +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. + + +""" +Unittests for graph.algorithms.heuristics +""" + + +import unittest +import pygraph +from pygraph.classes.graph import graph +from pygraph.classes.digraph import digraph +from pygraph.algorithms.heuristics.euclidean import euclidean +from pygraph.algorithms.heuristics.chow import chow +from pygraph.classes import exceptions + +from test_data import nations_of_the_world + + +class test_chow(unittest.TestCase): + + def setUp(self): + self.G = graph() + nations_of_the_world(self.G) + + def test_basic(self): + """ + Test some very basic functionality + """ + englands_neighbors = self.G.neighbors("England") + assert set(['Wales', 'Scotland', 'France', 'Ireland']) == set( englands_neighbors ) + + def test_chow(self): + heuristic = chow( "Wales", "North Korea", "Russia" ) + heuristic.optimize(self.G) + result = pygraph.algorithms.minmax.heuristic_search( self.G, "England", "India", heuristic ) + + def test_chow_unreachable(self): + heuristic = chow( "Wales", "North Korea", "Russia" ) + self.G.add_node("Sealand") + self.G.add_edge(("England", "Sealand")) + heuristic.optimize(self.G) + self.G.del_edge(("England", "Sealand")) + + try: + result = pygraph.algorithms.minmax.heuristic_search( self.G, "England", "Sealand" , heuristic ) + except exceptions.NodeUnreachable as _: + return + + assert False, "This test should raise an unreachable error." + + +class test_euclidean(unittest.TestCase): + + def setUp(self): + self.G = pygraph.classes.graph.graph() + self.G.add_node('A', [('position',[0,0])]) + self.G.add_node('B', [('position',[2,0])]) + self.G.add_node('C', [('position',[2,3])]) + self.G.add_node('D', [('position',[1,2])]) + self.G.add_edge(('A', 'B'), wt=4) + self.G.add_edge(('A', 'D'), wt=5) + self.G.add_edge(('B', 'C'), wt=9) + self.G.add_edge(('D', 'C'), wt=2) + + def test_euclidean(self): + heuristic = euclidean() + heuristic.optimize(self.G) + result = pygraph.algorithms.minmax.heuristic_search(self.G, 'A', 'C', heuristic ) + assert result == ['A', 'D', 'C'] + +if __name__ == "__main__": + unittest.main() \ No newline at end of file diff --git a/tests/unittests-hypergraph.py b/tests/unittests-hypergraph.py index 0d82a0c..6195f52 100644 --- a/tests/unittests-hypergraph.py +++ b/tests/unittests-hypergraph.py @@ -32,7 +32,7 @@ from pygraph.algorithms.generators import generate from pygraph.classes.exceptions import AdditionError from pygraph.classes.hypergraph import hypergraph -import testlib +from . import testlib from copy import copy, deepcopy class test_hypergraph(unittest.TestCase): diff --git a/tests/unittests-hypergraph.py.bak b/tests/unittests-hypergraph.py.bak new file mode 100644 index 0000000..0d82a0c --- /dev/null +++ b/tests/unittests-hypergraph.py.bak @@ -0,0 +1,339 @@ +# Copyright (c) Pedro Matiello +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, +# copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following +# conditions: + +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. + + +""" +Unittests for graph.classes.hypergraph +""" + + +import unittest +import pygraph +from pygraph.algorithms.generators import generate +from pygraph.classes.exceptions import AdditionError +from pygraph.classes.hypergraph import hypergraph +import testlib +from copy import copy, deepcopy + +class test_hypergraph(unittest.TestCase): + + # Add/Remove nodes and edges + + def test_raise_exception_on_duplicate_node_addition(self): + gr = hypergraph() + gr.add_node('a_node') + try: + gr.add_node('a_node') + except AdditionError: + pass + else: + fail() + + def test_raise_exception_on_duplicate_edge_link(self): + gr = hypergraph() + gr.add_node('a node') + gr.add_hyperedge('an edge') + gr.link('a node', 'an edge') + try: + gr.link('a node', 'an edge') + except AdditionError: + pass + else: + fail() + + def test_raise_exception_on_non_existing_link_removal(self): + gr = hypergraph() + gr.add_node(0) + gr.add_hyperedge(1) + try: + gr.unlink(0, 1) + except ValueError: + pass + else: + fail() + + def test_raise_exception_when_edge_added_from_non_existing_node(self): + gr = hypergraph() + gr.add_nodes([0,1]) + try: + gr.link(3,0) + except KeyError: + pass + else: + fail() + assert gr.neighbors(0) == [] + + def test_raise_exception_when_edge_added_to_non_existing_node(self): + gr = hypergraph() + gr.add_nodes([0,1]) + try: + gr.link(0,3) + except KeyError: + pass + else: + fail() + assert gr.neighbors(0) == [] + + def test_remove_node(self): + gr = testlib.new_hypergraph() + gr.del_node(0) + self.assertTrue(0 not in gr.nodes()) + for e in gr.hyperedges(): + for n in gr.links(e): + self.assertTrue(n in gr.nodes()) + + def test_remove_edge(self): + h = hypergraph() + h.add_nodes([1,2]) + h.add_edges(['a', 'b']) + + h.link(1,'a') + h.link(2,'a') + h.link(1,'b') + h.link(2,'b') + + # Delete an edge + h.del_edge('a') + + assert 1 == len(h.hyperedges()) + + gr = testlib.new_hypergraph() + edge_no = len(gr.nodes())+1 + gr.del_hyperedge(edge_no) + self.assertTrue(edge_no not in gr.hyperedges()) + + def test_remove_link_from_node_to_same_node(self): + gr = hypergraph() + gr.add_node(0) + gr.add_hyperedge(0) + gr.link(0, 0) + gr.unlink(0, 0) + + def test_remove_node_with_edge_to_itself(self): + gr = hypergraph() + gr.add_node(0) + gr.add_hyperedge(0) + gr.link(0, 0) + gr.del_node(0) + + def test_check_add_node_s(self): + gr = hypergraph() + nodes = [1,2,3] + gr.add_nodes(nodes) + gr.add_node(0) + + for n in [0] + nodes: + assert n in gr + assert gr.has_node(n) + + def test_rank(self): + # Uniform case + gr = testlib.new_uniform_hypergraph(3) + assert 3 == gr.rank() + + # Non-uniform case + gr = testlib.new_hypergraph() + num = max([len(gr.links(e)) for e in gr.hyperedges()]) + assert num == gr.rank() + + def test_repr(self): + """ + Validate the repr string + """ + gr = testlib.new_hypergraph() + gr_repr = repr(gr) + assert isinstance(gr_repr, str ) + assert gr.__class__.__name__ in gr_repr + + def test_order_len_equivlance(self): + """ + Verify the behavior of G.order() + """ + gr = testlib.new_hypergraph() + assert len(gr) == gr.order() + assert gr.order() == len( gr.node_links ) + + def test_hypergraph_equality_nodes(self): + """ + Hyperaph equality test. This one checks node equality. + """ + gr = hypergraph() + gr.add_nodes([0,1,2,3,4,5]) + + gr2 = deepcopy(gr) + + gr3 = deepcopy(gr) + gr3.del_node(5) + + gr4 = deepcopy(gr) + gr4.add_node(6) + gr4.del_node(0) + + assert gr == gr2 + assert gr2 == gr + assert gr != gr3 + assert gr3 != gr + assert gr != gr4 + assert gr4 != gr + + def test_hypergraph_equality_edges(self): + """ + Hyperaph equality test. This one checks edge equality. + """ + gr = hypergraph() + gr.add_nodes([0,1,2,3]) + gr.add_edge('e1') + gr.add_edge('e2') + gr.link(0, 'e1') + gr.link(1, 'e1') + gr.link(1, 'e2') + gr.link(2, 'e2') + + gr2 = deepcopy(gr) + + gr3 = deepcopy(gr) + gr3.del_edge('e2') + + gr4 = deepcopy(gr) + gr4.unlink(1, 'e2') + + assert gr == gr2 + assert gr2 == gr + assert gr != gr3 + assert gr3 != gr + assert gr != gr4 + assert gr4 != gr + + def test_hypergraph_equality_labels(self): + """ + Hyperaph equality test. This one checks edge equality. + """ + gr = hypergraph() + gr.add_nodes([0,1,2,3]) + gr.add_edge('e1') + gr.add_edge('e2') + gr.add_edge('e3') + gr.set_edge_label('e1', 'l1') + gr.set_edge_label('e2', 'l2') + + gr2 = deepcopy(gr) + + gr3 = deepcopy(gr) + gr3.set_edge_label('e3', 'l3') + + gr4 = deepcopy(gr) + gr4.set_edge_label('e1', 'lx') + + gr5 = deepcopy(gr) + gr5.del_edge('e1') + gr5.add_edge('e1') + + assert gr == gr2 + assert gr2 == gr + assert gr != gr3 + assert gr3 != gr + assert gr != gr4 + assert gr4 != gr + assert gr != gr5 + assert gr5 != gr + + def test_hypergraph_equality_attributes(self): + """ + Hyperaph equality test. This one checks edge equality. + """ + gr = hypergraph() + gr.add_nodes([0,1]) + gr.add_edge('e1') + gr.add_edge('e2') + gr.add_node_attribute(0, ('a',0)) + gr.add_edge_attribute('e1', ('b',1)) + + gr2 = deepcopy(gr) + + gr3 = deepcopy(gr) + gr3.add_node_attribute(0, ('x','y')) + + gr4 = deepcopy(gr) + gr4.add_edge_attribute('e1', ('u','v')) + + gr5 = deepcopy(gr) + gr5.del_edge('e1') + gr5.add_edge('e1') + + gr6 = deepcopy(gr) + gr6.del_node(0) + gr6.add_node(0) + + assert gr == gr2 + assert gr2 == gr + assert gr != gr3 + assert gr3 != gr + assert gr != gr4 + assert gr4 != gr + assert gr != gr5 + assert gr5 != gr + assert gr != gr6 + assert gr6 != gr + + def test_hypergraph_equality_weight(self): + """ + Hyperaph equality test. This one checks edge equality. + """ + gr = hypergraph() + gr.add_nodes([0,1,2,3]) + gr.add_edge('e1') + gr.add_edge('e2') + gr.add_edge('e3') + gr.set_edge_weight('e1', 2) + + gr2 = deepcopy(gr) + + gr3 = deepcopy(gr) + gr3.set_edge_weight('e3', 2) + + gr4 = deepcopy(gr) + gr4.set_edge_weight('e1', 1) + + assert gr == gr2 + assert gr2 == gr + assert gr != gr3 + assert gr3 != gr + assert gr != gr4 + assert gr4 != gr + + def test_hypergraph_link_unlink_link(self): + """ + Hypergraph link-unlink-link test. It makes sure that unlink cleans + everything properly. No AdditionError should occur. + """ + h = hypergraph() + h.add_nodes([1,2]) + h.add_edges(['e1']) + + h.link(1, 'e1') + h.unlink(1, 'e1') + h.link(1,'e1') + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/unittests-minmax.py b/tests/unittests-minmax.py index 8442af1..a887848 100644 --- a/tests/unittests-minmax.py +++ b/tests/unittests-minmax.py @@ -28,7 +28,7 @@ """ import unittest -import testlib +from . import testlib from pygraph.classes.graph import graph from pygraph.classes.digraph import digraph diff --git a/tests/unittests-minmax.py.bak b/tests/unittests-minmax.py.bak new file mode 100644 index 0000000..8442af1 --- /dev/null +++ b/tests/unittests-minmax.py.bak @@ -0,0 +1,230 @@ +# Copyright (c) Pedro Matiello +# Johannes Reinhardt +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, +# copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following +# conditions: + +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. + + +""" +Unittests for graph.algorithms.searching +""" + +import unittest +import testlib + +from pygraph.classes.graph import graph +from pygraph.classes.digraph import digraph + +from pygraph.algorithms.searching import depth_first_search +from pygraph.algorithms.minmax import minimal_spanning_tree,\ +shortest_path, heuristic_search, shortest_path_bellman_ford, maximum_flow, cut_tree +from pygraph.algorithms.heuristics.chow import chow +from pygraph.classes.exceptions import NegativeWeightCycleError + +from copy import deepcopy + +# helpers + +def tree_weight(gr, tree): + sum = 0; + for each in tree: + sum = sum + gr.edge_weight((each, tree[each])) + return sum + +def add_spanning_tree(gr, st): + # A very tolerant implementation. + gr.add_nodes(list(st.keys())) + for each in st: + if ((st[each] is not None) and (not gr.has_edge((st[each], each)))): # Accepts invalid STs + gr.add_edge((st[each], each)) + +def bf_path(gr, root, target, remainder): + if (remainder <= 0): return True + if (root == target): return False + for each in gr[root]: + if (not bf_path(gr, each, target, remainder - gr.edge_weight((root, each)))): + return False + return True + +def generate_fixture_digraph(): + #helper for bellman-ford algorithm + G = digraph() + G.add_nodes([1,2,3,4,5]) + G.add_edge((1,2), 6) + G.add_edge((1,4), 7) + G.add_edge((2,4), 8) + G.add_edge((3,2), -2) + G.add_edge((4,3), -3) + G.add_edge((2,5), -4) + G.add_edge((4,5), 9) + G.add_edge((5,1), 2) + G.add_edge((5,3), 7) + return G + +def generate_fixture_digraph_neg_weight_cycle(): + #graph with a neg. weight cycle + G = generate_fixture_digraph() + G.del_edge((2,4)) + G.add_edge((2,4), 2)#changed + + G.add_nodes([100,200]) #unconnected part + G.add_edge((100,200),2) + return G + +def generate_fixture_digraph_unconnected(): + G = generate_fixture_digraph() + G.add_nodes([100,200]) + G.add_edge((100,200),2) + return G + +# minimal spanning tree tests + +class test_minimal_spanning_tree(unittest.TestCase): + + def test_minimal_spanning_tree_on_graph(self): + gr = testlib.new_graph(wt_range=(1,10)) + mst = minimal_spanning_tree(gr, root=0) + wt = tree_weight(gr, mst) + len_dfs = len(depth_first_search(gr, root=0)[0]) + for each in mst: + if (mst[each] != None): + mst_copy = deepcopy(mst) + del(mst_copy[each]) + for other in gr[each]: + mst_copy[each] = other + if (tree_weight(gr, mst_copy) < wt): + gr2 = graph() + add_spanning_tree(gr2, mst_copy) + assert len(depth_first_search(gr2, root=0)[0]) < len_dfs + + +# shortest path tests + +class test_shortest_path(unittest.TestCase): + + def test_shortest_path_on_graph(self): + gr = testlib.new_graph(wt_range=(1,10)) + st, dist = shortest_path(gr, 0) + for each in gr: + if (each in dist): + assert bf_path(gr, 0, each, dist[each]) + + def test_shortest_path_on_digraph(self): + # Test stub: not checking for correctness yet + gr = testlib.new_digraph(wt_range=(1,10)) + st, dist = shortest_path(gr, 0) + for each in gr: + if (each in dist): + assert bf_path(gr, 0, each, dist[each]) + + def test_shortest_path_should_fail_if_source_does_not_exist(self): + gr = testlib.new_graph() + try: + shortest_path(gr, 'invalid') + assert False + except (KeyError): + pass + +class test_shortest_path_bellman_ford(unittest.TestCase): + + def test_shortest_path_BF_on_empty_digraph(self): + pre, dist = shortest_path_bellman_ford(digraph(), 1) + assert pre == {1:None} and dist == {1:0} + + def test_shortest_path_BF_on_digraph(self): + #testing correctness on the fixture + gr = generate_fixture_digraph() + pre,dist = shortest_path_bellman_ford(gr, 1) + assert pre == {1: None, 2: 3, 3: 4, 4: 1, 5: 2} \ + and dist == {1: 0, 2: 2, 3: 4, 4: 7, 5: -2} + + def test_shortest_path_BF_on_digraph_with_negwcycle(self): + #test negative weight cycle detection + gr = generate_fixture_digraph_neg_weight_cycle() + self.assertRaises(NegativeWeightCycleError, + shortest_path_bellman_ford, gr, 1) + + def test_shortest_path_BF_on_unconnected_graph(self): + gr = generate_fixture_digraph_unconnected() + pre,dist = shortest_path_bellman_ford(gr, 100) + assert pre == {200: 100, 100: None} and \ + dist == {200: 2, 100: 0} + +class test_maxflow_mincut(unittest.TestCase): + + def test_trivial_maxflow(self): + gr = digraph() + gr.add_nodes([0,1,2,3]) + gr.add_edge((0,1), wt=5) + gr.add_edge((1,2), wt=3) + gr.add_edge((2,3), wt=7) + flows, cuts = maximum_flow(gr, 0, 3) + assert flows[(0,1)] == 3 + assert flows[(1,2)] == 3 + assert flows[(2,3)] == 3 + + def test_random_maxflow(self): + gr = testlib.new_digraph(wt_range=(1,20)) + flows, cuts = maximum_flow(gr, 0, 1) + # Sanity test + for each in flows: + assert gr.edge_weight(each) >= flows[each] + +# Tests for heuristic search are not necessary here as it's tested +# in unittests-heuristics.py + +class test_cut_tree(unittest.TestCase): + + def test_cut_tree(self): + #set up the graph (see example on wikipedia page for Gomory-Hu tree) + gr = graph() + gr.add_nodes([0,1,2,3,4,5]) + gr.add_edge((0,1), wt=1) + gr.add_edge((0,2), wt=7) + gr.add_edge((1,3), wt=3) + gr.add_edge((1,2), wt=1) + gr.add_edge((1,4), wt=2) + gr.add_edge((2,4), wt=4) + gr.add_edge((3,4), wt=1) + gr.add_edge((3,5), wt=6) + gr.add_edge((4,5), wt=2) + + ct = cut_tree(gr) + + #check ct + assert ct[(2,0)] == 8 + assert ct[(4,2)] == 6 + assert ct[(1,4)] == 7 + assert ct[(3,1)] == 6 + assert ct[(5,3)] == 8 + + def test_cut_tree_with_empty_graph(self): + gr = graph() + ct = cut_tree(gr) + assert ct == {} + + def test_cut_tree_with_random_graph(self): + gr = testlib.new_graph() + ct = cut_tree(gr) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/unittests-pagerank.py b/tests/unittests-pagerank.py index 8d68318..41c150e 100644 --- a/tests/unittests-pagerank.py +++ b/tests/unittests-pagerank.py @@ -30,7 +30,7 @@ import unittest from pygraph.classes.digraph import digraph from pygraph.algorithms.pagerank import pagerank -import testlib +from . import testlib class test_pagerank(unittest.TestCase): diff --git a/tests/unittests-pagerank.py.bak b/tests/unittests-pagerank.py.bak new file mode 100644 index 0000000..8d68318 --- /dev/null +++ b/tests/unittests-pagerank.py.bak @@ -0,0 +1,103 @@ +# Copyright (c) 2010 Pedro Matiello +# Juarez Bochi +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, +# copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following +# conditions: + +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. + + +""" +Unittests for pygraph.algorithms.pagerank +""" + +import unittest +from pygraph.classes.digraph import digraph +from pygraph.algorithms.pagerank import pagerank +import testlib + +class test_pagerank(unittest.TestCase): + + # pagerank algorithm + + def test_pagerank_empty(self): + #Test if an empty dict is returned for an empty graph + G = digraph() + self.assertEqual(pagerank(G), {}) + + def test_pagerank_cycle(self): + #Test if all nodes in a cycle graph have the same value + G = digraph() + G.add_nodes([1, 2, 3, 4, 5]) + G.add_edge((1, 2)) + G.add_edge((2, 3)) + G.add_edge((3, 4)) + G.add_edge((4, 5)) + G.add_edge((5, 1)) + self.assertEqual(pagerank(G), {1: 0.2, 2: 0.2, 3: 0.2, 4: 0.2, 5: 0.2}) + + def test_pagerank(self): + #Test example from wikipedia: http://en.wikipedia.org/wiki/File:Linkstruct3.svg + G = digraph() + G.add_nodes([1, 2, 3, 4, 5, 6, 7]) + G.add_edge((1, 2)) + G.add_edge((1, 3)) + G.add_edge((1, 4)) + G.add_edge((1, 5)) + G.add_edge((1, 7)) + G.add_edge((2, 1)) + G.add_edge((3, 1)) + G.add_edge((3, 2)) + G.add_edge((4, 2)) + G.add_edge((4, 3)) + G.add_edge((4, 5)) + G.add_edge((5, 1)) + G.add_edge((5, 3)) + G.add_edge((5, 4)) + G.add_edge((5, 6)) + G.add_edge((6, 1)) + G.add_edge((6, 5)) + G.add_edge((7, 5)) + expected_pagerank = { + 1: 0.280, + 2: 0.159, + 3: 0.139, + 4: 0.108, + 5: 0.184, + 6: 0.061, + 7: 0.069, + } + pr = pagerank(G) + for k in pr: + self.assertAlmostEqual(pr[k], expected_pagerank[k], places=3) + + def test_pagerank_random(self): + G = testlib.new_digraph() + md = 0.00001 + df = 0.85 + pr = pagerank(G, damping_factor=df, min_delta=md) + min_value = (1.0-df)/len(G) + for node in G: + expected = min_value + for each in G.incidents(node): + expected += (df * pr[each] / len(G.neighbors(each))) + assert abs(pr[node] - expected) < md + +if __name__ == "__main__": + unittest.main() diff --git a/tests/unittests-readwrite.py b/tests/unittests-readwrite.py index 59e1fd7..1107315 100644 --- a/tests/unittests-readwrite.py +++ b/tests/unittests-readwrite.py @@ -30,7 +30,7 @@ import unittest import pygraph from pygraph.readwrite import dot, markup -import testlib +from . import testlib def graph_equality(gr1, gr2): for each in gr1.nodes(): diff --git a/tests/unittests-readwrite.py.bak b/tests/unittests-readwrite.py.bak new file mode 100644 index 0000000..59e1fd7 --- /dev/null +++ b/tests/unittests-readwrite.py.bak @@ -0,0 +1,117 @@ +# Copyright (c) Pedro Matiello +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, +# copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following +# conditions: + +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. + + +""" +Unittests for graph.algorithms.readwrite +""" + + +import unittest +import pygraph +from pygraph.readwrite import dot, markup +import testlib + +def graph_equality(gr1, gr2): + for each in gr1.nodes(): + assert each in gr2.nodes() + for each in gr2.nodes(): + assert each in gr1.nodes() + for each in gr1.edges(): + assert each in gr2.edges() + for each in gr2.edges(): + assert each in gr1.edges() + +class test_readwrite_dot(unittest.TestCase): + + def test_dot_for_graph(self): + gr = testlib.new_graph() + dotstr = dot.write(gr) + gr1 = dot.read(dotstr) + dotstr = dot.write(gr1) + gr2 = dot.read(dotstr) + graph_equality(gr1, gr2) + assert len(gr.nodes()) == len(gr1.nodes()) + assert len(gr.edges()) == len(gr1.edges()) + + def test_dot_for_digraph(self): + gr = testlib.new_digraph() + dotstr = dot.write(gr) + gr1 = dot.read(dotstr) + dotstr = dot.write(gr1) + gr2 = dot.read(dotstr) + graph_equality(gr1, gr2) + assert len(gr.nodes()) == len(gr1.nodes()) + assert len(gr.edges()) == len(gr1.edges()) + + def test_dot_for_hypergraph(self): + gr = testlib.new_hypergraph() + dotstr = dot.write(gr) + gr1 = dot.read_hypergraph(dotstr) + dotstr = dot.write(gr1) + gr2 = dot.read_hypergraph(dotstr) + graph_equality(gr1, gr2) + + def test_output_names_in_dot(self): + gr1 = testlib.new_graph() + gr1.name = "Some name 1" + gr2 = testlib.new_digraph() + gr2.name = "Some name 2" + gr3 = testlib.new_hypergraph() + gr3.name = "Some name 3" + assert "Some name 1" in dot.write(gr1) + assert "Some name 2" in dot.write(gr2) + assert "Some name 3" in dot.write(gr3) + +class test_readwrite_markup(unittest.TestCase): + + def test_xml_for_graph(self): + gr = testlib.new_graph() + dotstr = markup.write(gr) + gr1 = markup.read(dotstr) + dotstr = markup.write(gr1) + gr2 = markup.read(dotstr) + graph_equality(gr1, gr2) + assert len(gr.nodes()) == len(gr1.nodes()) + assert len(gr.edges()) == len(gr1.edges()) + + def test_xml_digraph(self): + gr = testlib.new_digraph() + dotstr = markup.write(gr) + gr1 = markup.read(dotstr) + dotstr = markup.write(gr1) + gr2 = markup.read(dotstr) + graph_equality(gr1, gr2) + assert len(gr.nodes()) == len(gr1.nodes()) + assert len(gr.edges()) == len(gr1.edges()) + + def test_xml_hypergraph(self): + gr = testlib.new_hypergraph() + dotstr = markup.write(gr) + gr1 = markup.read(dotstr) + dotstr = markup.write(gr1) + gr2 = markup.read(dotstr) + graph_equality(gr1, gr2) + +if __name__ == "__main__": + unittest.main() diff --git a/tests/unittests-searching.py b/tests/unittests-searching.py index fbb5b2e..a6e53e3 100644 --- a/tests/unittests-searching.py +++ b/tests/unittests-searching.py @@ -33,7 +33,7 @@ import pygraph.classes from pygraph.algorithms.searching import depth_first_search, breadth_first_search from sys import getrecursionlimit -import testlib +from . import testlib class test_depth_first_search(unittest.TestCase): @@ -74,7 +74,7 @@ def test_dfs_in_digraph(self): def test_dfs_very_deep_graph(self): gr = pygraph.classes.graph.graph() - gr.add_nodes(range(0,20001)) + gr.add_nodes(list(range(0,20001))) for i in range(0,20000): gr.add_edge((i,i+1)) recursionlimit = getrecursionlimit() diff --git a/tests/unittests-searching.py.bak b/tests/unittests-searching.py.bak new file mode 100644 index 0000000..fbb5b2e --- /dev/null +++ b/tests/unittests-searching.py.bak @@ -0,0 +1,118 @@ +# Copyright (c) Pedro Matiello +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, +# copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following +# conditions: + +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. + + +""" +Unittests for graph.algorithms.searching +""" + + +# Imports +import unittest +import pygraph +import pygraph.classes +from pygraph.algorithms.searching import depth_first_search, breadth_first_search +from sys import getrecursionlimit +import testlib + + +class test_depth_first_search(unittest.TestCase): + + def test_dfs_in_empty_graph(self): + gr = pygraph.classes.graph.graph() + st, pre, post = depth_first_search(gr) + assert st == {} + assert pre == [] + assert post == [] + + def test_dfs_in_graph(self): + gr = testlib.new_graph() + st, pre, post = depth_first_search(gr) + for each in gr: + if (st[each] != None): + assert pre.index(each) > pre.index(st[each]) + assert post.index(each) < post.index(st[each]) + for node in st: + assert gr.has_edge((st[node], node)) or st[node] == None + + def test_dfs_in_empty_digraph(self): + gr = pygraph.classes.digraph.digraph() + st, pre, post = depth_first_search(gr) + assert st == {} + assert pre == [] + assert post == [] + + def test_dfs_in_digraph(self): + gr = testlib.new_digraph() + st, pre, post = depth_first_search(gr) + for each in gr: + if (st[each] != None): + assert pre.index(each) > pre.index(st[each]) + assert post.index(each) < post.index(st[each]) + for node in st: + assert gr.has_edge((st[node], node)) or st[node] == None + + def test_dfs_very_deep_graph(self): + gr = pygraph.classes.graph.graph() + gr.add_nodes(range(0,20001)) + for i in range(0,20000): + gr.add_edge((i,i+1)) + recursionlimit = getrecursionlimit() + depth_first_search(gr, 0) + assert getrecursionlimit() == recursionlimit + +class test_breadth_first_search(unittest.TestCase): + + def test_bfs_in_empty_graph(self): + gr = pygraph.classes.graph.graph() + st, lo = breadth_first_search(gr) + assert st == {} + assert lo == [] + + def test_bfs_in_graph(self): + gr = pygraph.classes.graph.graph() + gr = testlib.new_digraph() + st, lo = breadth_first_search(gr) + for each in gr: + if (st[each] != None): + assert lo.index(each) > lo.index(st[each]) + for node in st: + assert gr.has_edge((st[node], node)) or st[node] == None + + def test_bfs_in_empty_digraph(self): + gr = pygraph.classes.digraph.digraph() + st, lo = breadth_first_search(gr) + assert st == {} + assert lo == [] + + def test_bfs_in_digraph(self): + gr = testlib.new_digraph() + st, lo = breadth_first_search(gr) + for each in gr: + if (st[each] != None): + assert lo.index(each) > lo.index(st[each]) + for node in st: + assert gr.has_edge((st[node], node)) or st[node] == None + +if __name__ == "__main__": + unittest.main() \ No newline at end of file diff --git a/tests/unittests-sorting.py b/tests/unittests-sorting.py index 24bedbc..0038657 100644 --- a/tests/unittests-sorting.py +++ b/tests/unittests-sorting.py @@ -32,7 +32,7 @@ from pygraph.algorithms.sorting import topological_sorting from pygraph.algorithms.searching import depth_first_search from sys import getrecursionlimit -import testlib +from . import testlib class test_topological_sorting(unittest.TestCase): @@ -79,7 +79,7 @@ def is_ordered(node, list): def test_topological_sort_on_very_deep_graph(self): gr = pygraph.classes.graph.graph() - gr.add_nodes(range(0,20001)) + gr.add_nodes(list(range(0,20001))) for i in range(0,20000): gr.add_edge((i,i+1)) recursionlimit = getrecursionlimit() diff --git a/tests/unittests-sorting.py.bak b/tests/unittests-sorting.py.bak new file mode 100644 index 0000000..24bedbc --- /dev/null +++ b/tests/unittests-sorting.py.bak @@ -0,0 +1,90 @@ +# Copyright (c) Pedro Matiello +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, +# copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following +# conditions: + +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. + + +""" +Unittests for graph.algorithms.sorting +""" + + +import unittest +import pygraph.classes +from pygraph.algorithms.sorting import topological_sorting +from pygraph.algorithms.searching import depth_first_search +from sys import getrecursionlimit +import testlib + + +class test_topological_sorting(unittest.TestCase): + + def test_topological_sorting_on_tree(self): + gr = testlib.new_graph() + st, pre, post = depth_first_search(gr) + tree = pygraph.classes.digraph.digraph() + + + for each in st: + if st[each]: + if (each not in tree.nodes()): + tree.add_node(each) + if (st[each] not in tree.nodes()): + tree.add_node(st[each]) + tree.add_edge((st[each], each)) + + ts = topological_sorting(tree) + for each in ts: + if (st[each]): + assert ts.index(each) > ts.index(st[each]) + + def test_topological_sorting_on_digraph(self): + + def is_ordered(node, list): + # Has parent on list + for each in list: + if gr.has_edge((each, node)): + return True + # Has no possible ancestors on list + st, pre, post = depth_first_search(gr, node) + for each in list: + if (each in st): + return False + return True + + gr = testlib.new_digraph() + ts = topological_sorting(gr) + + while (ts): + x = ts.pop() + assert is_ordered(x, ts) + + def test_topological_sort_on_very_deep_graph(self): + gr = pygraph.classes.graph.graph() + gr.add_nodes(range(0,20001)) + for i in range(0,20000): + gr.add_edge((i,i+1)) + recursionlimit = getrecursionlimit() + topological_sorting(gr) + assert getrecursionlimit() == recursionlimit + +if __name__ == "__main__": + unittest.main() \ No newline at end of file