class TestGraphAddEdgeExceptionWarning(UnittestPythonCompatibility): """ Test logged warnings and raised Exceptions by Graph add_edge. Same as for add_nodes """ def setUp(self): """ Build empty graph to add a node to and test default state """ self.graph = Graph() def test_add_edge_node_not_exist(self): """ Test adding edges for nodes that do not exist """ self.assertRaises(GraphitException, self.graph.add_edge, 1, 2) def test_add_edge_exist(self): """ Test adding edges that already exist. A warning is logged but edge ID is returned """ self.graph.add_nodes([1, 2]) self.graph.add_edge(1, 2) eid = self.graph.add_edge(1, 2) self.assertTrue(eid, (1, 2)) def test_remove_edge_exist(self): """ Test removal of edges that do not exist should only log a warning """ self.graph.add_nodes([1, 2]) try: self.graph.remove_edge(1, 2) except GraphitException: self.fail('remove_edge raised GraphitException unexpectedly')
class TestGraphAddEdgeAttributes(UnittestPythonCompatibility): """ Test additional attribute storage for Graph add_edge """ def setUp(self): """ Build Graph with a few nodes but no edges yet """ self.graph = Graph() self.graph.add_nodes('graph') # Only nodes no edges yet self.assertTrue(len(self.graph) == 5) self.assertTrue(len(self.graph.nodes) == 5) self.assertTrue(len(self.graph.edges) == 0) self.assertTrue(len(self.graph.adjacency) == 5) self.assertTrue(all([len(a) == 0 for a in self.graph.adjacency.values()])) # auto_nid self.assertTrue(self.graph.data.auto_nid) def tearDown(self): """ Test state after edge attribute addition """ # If undirected, add reversed edges if not self.graph.directed: for edge, attr in list(self.edges.items()): self.edges[edge[::-1]] = attr for edge, attr in self.edges.items(): self.assertTrue(edge in self.graph.edges) self.assertDictEqual(self.graph.edges[edge], attr) def test_add_edge_no_attribute(self): """ No attributes added should yield empty dict """ self.edges = {(1, 2): {}} for edge, attr in self.edges.items(): self.graph.add_edge(*edge, **attr) def test_add_edge_single_attribute_undirected(self): """ Add a single attribute :return: """ self.edges = {(1, 2): {'weight': 2.33}} for edge, attr in self.edges.items(): self.graph.add_edge(*edge, **attr) def test_add_edge_multiple_attributes_undirected(self): """ Add a single attribute """ self.edges = {(1, 2): {'test': True, 'pv': 1.44}} for edge, attr in self.edges.items(): self.graph.add_edge(*edge, **attr) def test_add_edge_nested_attribute_undirected(self): """ Test adding nested attributed, e.a. dict in dict """ self.edges = {(1, 2): {'func': len, 'nested': {'weight': 1.22, 'leaf': True}}} for edge, attr in self.edges.items(): self.graph.add_edge(*edge, **attr) def test_add_edge_single_attribute_directed(self): """ Add a single attribute, directed :return: """ self.graph.directed = True self.edges = {(1, 2): {'weight': 2.33}} for edge, attr in self.edges.items(): self.graph.add_edge(*edge, **attr) def test_add_edge_multiple_attributes_directed(self): """ Add a single attribute, directed """ self.graph.directed = True self.edges = {(1, 2): {'test': True, 'pv': 1.44}} for edge, attr in self.edges.items(): self.graph.add_edge(*edge, **attr) def test_add_edge_nested_attribute_directed(self): """ Test adding nested attributed, e.a. dict in dict, directed """ self.graph.directed = True self.edges = {(1, 2): {'func': len, 'nested': {'weight': 1.22, 'leaf': True}}} for edge, attr in self.edges.items(): self.graph.add_edge(*edge, **attr) def test_add_edge_single_attribute_mixed(self): """ Add a single attribute, override undirected graph """ self.edges = {(1, 2): {'weight': 2.33}} for edge, attr in self.edges.items(): self.graph.add_edge(*edge, directed=True, **attr) self.graph.directed = True
class TestGraphAddEdge(UnittestPythonCompatibility): """ Test Graph add_edge method with the Graph.auto_nid set to False mimicking the behaviour of many popular graph packages """ def setUp(self): """ Build Graph with a few nodes but no edges yet """ self.graph = Graph(auto_nid=False) self.graph.add_nodes('graph') # Only nodes no edges yet self.assertTrue(len(self.graph) == 5) self.assertTrue(len(self.graph.nodes) == 5) self.assertTrue(len(self.graph.edges) == 0) self.assertTrue(len(self.graph.adjacency) == 5) self.assertTrue(all([len(a) == 0 for a in self.graph.adjacency.values()])) # auto_nid self.assertFalse(self.graph.data.auto_nid) def tearDown(self): """ Test state after edge addition """ # If undirected, add reversed edges if not self.graph.directed: self.edges.extend([e[::-1] for e in self.edges if e[::-1] not in self.edges]) for edge in self.edges: # Edge should be present self.assertTrue(edge in self.graph.edges) # Nodes connected should be present self.assertTrue(all([node in self.graph.nodes for node in edge])) # Adjacency setup self.assertTrue(edge[1] in self.graph.adjacency[edge[0]]) # If directional, reverse edge not in graph if self.graph.directed: rev_edge = edge[::-1] if rev_edge not in self.edges: self.assertTrue(rev_edge not in self.graph.edges) # filled after addition self.assertTrue(len(self.graph) == 5) self.assertTrue(len(self.graph.nodes) == 5) self.assertTrue(len(self.graph.edges) == len(self.edges)) self.assertTrue(len(self.graph.adjacency) == 5) def test_add_edge_undirectional(self): """ Test adding a single un-directional edge """ self.edges = [('g', 'r')] self.graph.add_edge(*self.edges[0]) def test_add_edges_undirectional(self): """ Test adding multiple un-directional edges """ self.edges = [('g', 'r'), ('r', 'a'), ('a', 'p'), ('a', 'h'), ('p', 'h')] self.graph.add_edges(self.edges) def test_add_edge_directional(self): """ Test adding a single directional edge """ self.edges = [('g', 'r')] self.graph.directed = False self.graph.add_edge(*self.edges[0]) def test_add_edges_directional(self): """ Test adding multiple directional edges """ self.edges = [('g', 'r'), ('r', 'a'), ('a', 'p'), ('a', 'h'), ('p', 'h')] self.graph.directed = False self.graph.add_edges(self.edges) def test_add_nodes_and_edges(self): """ Test adding edges and creating the nodes from the edges. Auto_nid is set to False automatically to force identical node/edge names. When node exists, no message. """ self.graph = Graph() self.edges = [('g', 'r'), ('r', 'a'), ('a', 'p'), ('a', 'h'), ('p', 'h')] self.graph.add_edges(self.edges, node_from_edge=True)
class TestGraphEdgeDirectionality(UnittestPythonCompatibility): """ Test adding edges in directional or un-directional way """ def setUp(self): """ Build Graph with a few nodes but no edges yet """ self.graph = Graph() self.graph.add_nodes([1, 2, 3]) # Only nodes no edges yet self.assertTrue(len(self.graph) == 3) self.assertTrue(len(self.graph.nodes) == 3) self.assertTrue(len(self.graph.edges) == 0) self.assertTrue(len(self.graph.adjacency) == 3) self.assertTrue(all([len(a) == 0 for a in self.graph.adjacency.values()])) def tearDown(self): """ Test state after edge addition """ # If undirected, add reversed edges if not self.graph.directed: self.edges.extend([e[::-1] for e in self.edges if e[::-1] not in self.edges]) for edge in self.edges: # Edge should be present self.assertTrue(edge in self.graph.edges) # Nodes connected should be present self.assertTrue(all([node in self.graph.nodes for node in edge])) # Adjacency setup self.assertTrue(edge[1] in self.graph.adjacency[edge[0]]) # If directional, reverse edge not in graph if self.graph.directed: rev_edge = edge[::-1] if rev_edge not in self.edges: self.assertTrue(rev_edge not in self.graph.edges) # filled after addition self.assertTrue(len(self.graph) == 3) self.assertTrue(len(self.graph.nodes) == 3) self.assertTrue(len(self.graph.edges) == len(self.edges)) self.assertTrue(len(self.graph.adjacency) == 3) def test_add_edge_undirectional_graph(self): """ Test default add edge in undirected graph """ self.graph.directed = False self.edges = [(1, 2), (2, 3)] self.graph.add_edges(self.edges) def test_add_edge_directional_graph(self): """ Test default add edge in directed graph """ self.graph.directed = True self.edges = [(1, 2), (2, 3)] self.graph.add_edges(self.edges) def test_add_edge_mixed_graph(self): """ Add edges with local override in directionality yielding a mixed directional graph """ self.edges = [(1, 2), (2, 3), (3, 2)] self.graph.add_edge(1, 2, directed=True) self.graph.add_edge(2, 3) self.graph.directed = True
class TestGraphAlgorithms(UnittestPythonCompatibility): def setUp(self): edges = { (1, 2): { 'weight': 1.0 }, (2, 3): { 'weight': 1.0 }, (2, 4): { 'weight': 1.0 }, (3, 5): { 'weight': 1.0 }, (4, 5): { 'weight': 1.0 }, (4, 7): { 'weight': 1.0 }, (5, 7): { 'weight': 0.75 }, (3, 14): { 'weight': 2.0 }, (14, 15): { 'weight': 2.0 }, (14, 16): { 'weight': 1.0 }, (15, 12): { 'weight': 2.0 }, (22, 24): { 'weight': 1.0 }, (12, 13): { 'weight': 2.0 }, (13, 28): { 'weight': 1.0 }, (5, 8): { 'weight': 1.0 }, (8, 9): { 'weight': 0.5 }, (8, 10): { 'weight': 3.0 }, (9, 12): { 'weight': 0.5 }, (10, 11): { 'weight': 3.0 }, (11, 13): { 'weight': 1.0 }, (7, 25): { 'weight': 1.0 }, (25, 26): { 'weight': 1.0 }, (26, 27): { 'weight': 1.0 }, (11, 26): { 'weight': 1.0 }, (7, 17): { 'weight': 1.0 }, (17, 18): { 'weight': 1.0 }, (18, 19): { 'weight': 2.0 }, (18, 20): { 'weight': 1.0 }, (20, 21): { 'weight': 1.0 }, (21, 22): { 'weight': 1.0 }, (22, 23): { 'weight': 1.0 } } # Regular graphit Graph self.graph = Graph(auto_nid=False) self.graph.directed = True # NetworkX graphit Graph self.gn = NetworkXGraph(auto_nid=False) self.gn.directed = True # NetworkX graph #self.nx = networkx.DiGraph() for eid in edges: self.graph.add_edge(node_from_edge=True, *eid, **edges[eid]) self.gn.add_edge(node_from_edge=True, *eid, **edges[eid]) #self.nx.add_edge(*eid, **edges[eid]) def test_algorithm_degree(self): """ Test degree method part of the Adjacency view """ # Degree self.assertDictEqual( self.graph.adjacency.degree(), { 1: 1, 2: 3, 3: 3, 4: 3, 5: 4, 7: 4, 14: 3, 15: 2, 16: 1, 12: 3, 22: 3, 24: 1, 13: 3, 28: 1, 8: 3, 9: 2, 10: 2, 11: 3, 25: 2, 26: 3, 27: 1, 17: 2, 18: 3, 19: 1, 20: 2, 21: 2, 23: 1 }) # Outdegree self.assertDictEqual( self.graph.adjacency.degree(method='outdegree'), { 1: 1, 2: 2, 3: 2, 4: 2, 5: 2, 7: 2, 14: 2, 15: 1, 16: 0, 12: 1, 22: 2, 24: 0, 13: 1, 28: 0, 8: 2, 9: 1, 10: 1, 11: 2, 25: 1, 26: 1, 27: 0, 17: 1, 18: 2, 19: 0, 20: 1, 21: 1, 23: 0 }) # Indegree self.assertDictEqual( self.graph.adjacency.degree(method='indegree'), { 1: 0, 2: 1, 3: 1, 4: 1, 5: 2, 7: 2, 14: 1, 15: 1, 16: 1, 12: 2, 22: 1, 24: 1, 13: 2, 28: 1, 8: 1, 9: 1, 10: 1, 11: 1, 25: 1, 26: 2, 27: 1, 17: 1, 18: 1, 19: 1, 20: 1, 21: 1, 23: 1 }) # Weighted degree self.assertDictEqual( self.graph.adjacency.degree(weight='weight'), { 1: 1.0, 2: 3.0, 3: 4.0, 4: 3.0, 5: 3.75, 7: 3.75, 14: 5.0, 15: 4.0, 16: 1.0, 12: 4.5, 22: 3.0, 24: 1.0, 13: 4.0, 28: 1.0, 8: 4.5, 9: 1.0, 10: 6.0, 11: 5.0, 25: 2.0, 26: 3.0, 27: 1.0, 17: 2.0, 18: 4.0, 19: 2.0, 20: 2.0, 21: 2.0, 23: 1.0 }) def test_algorithm_dijkstra_shortest_path(self): """ Test Dijkstra shortest path method, weighted and non-weighted. """ # Shortest path in fully directed graph self.assertListEqual(dijkstra_shortest_path(self.graph, 1, 28), [1, 2, 3, 14, 15, 12, 13, 28]) # Shortest path in fully directed graph considering weights self.assertListEqual( dijkstra_shortest_path(self.graph, 1, 28, weight='weight'), [1, 2, 3, 5, 8, 9, 12, 13, 28]) # Reverse directionality of edge (14, 15) self.graph.add_edge(15, 14) self.graph.remove_edge(14, 15) self.assertListEqual(dijkstra_shortest_path(self.graph, 1, 28), [1, 2, 3, 5, 8, 10, 11, 13, 28]) def test_algorithm_dfs_paths(self): """ Test depth-first search of all paths between two nodes """ self.assertListEqual( list(dfs_paths(self.graph, 2, 26)), [[2, 4, 7, 25, 26], [2, 4, 5, 7, 25, 26], [2, 4, 5, 8, 10, 11, 26], [2, 3, 5, 7, 25, 26], [2, 3, 5, 8, 10, 11, 26]]) # Nodes 13 and 26 not connected via directional path self.assertListEqual(list(dfs_paths(self.graph, 13, 26)), []) # Switch to breath-first search self.assertListEqual( list(dfs_paths(self.graph, 2, 26, method='bfs')), [[2, 4, 7, 25, 26], [2, 3, 5, 7, 25, 26], [2, 4, 5, 7, 25, 26], [2, 3, 5, 8, 10, 11, 26], [2, 4, 5, 8, 10, 11, 26]]) # dfs_path with path length cutoff self.assertListEqual( list(dfs_paths(self.graph, 2, 26, cutoff=5)), [[2, 4, 7, 25, 26], [2, 4, 5, 7, 25, 26], [2, 3, 5, 7, 25, 26]]) def test_algorithm_dfs_edges(self): """ Test graph dfs_edges method in depth-first-search (dfs) and breath-first-search (bfs) mode. """ self.assertListEqual(list(dfs_edges(self.graph, 5)), [(5, 7), (7, 17), (17, 18), (18, 19), (18, 20), (20, 21), (21, 22), (22, 23), (22, 24), (7, 25), (25, 26), (26, 27), (5, 8), (8, 9), (9, 12), (12, 13), (13, 28), (8, 10), (10, 11)]) self.assertListEqual(list(dfs_edges(self.graph, 8)), [(8, 9), (9, 12), (12, 13), (13, 28), (8, 10), (10, 11), (11, 26), (26, 27)]) # Breath-first search self.assertListEqual(list(dfs_edges(self.graph, 8, method='bfs')), [(8, 9), (8, 10), (9, 12), (10, 11), (12, 13), (11, 26), (13, 28), (26, 27)]) # With depth limit self.assertListEqual(list(dfs_edges(self.graph, 5, max_depth=2)), [(5, 7), (7, 17), (7, 25), (5, 8), (8, 9), (8, 10)]) def test_algorithm_dfs_edges__edge_based(self): """ Test graph dfs_edges in True edge traversal mode """ # Use true edge oriented DFS method self.assertListEqual(list(dfs_edges(self.graph, 8, edge_based=True)), [(8, 9), (9, 12), (12, 13), (13, 28), (8, 10), (10, 11), (11, 13), (11, 26), (26, 27)]) def test_algorithm_dfs_nodes(self): """ Test graph dfs_nodes method in depth-first-search (dfs) and breath-first-search (bfs) mode """ # Connectivity information using Depth First Search / Breath first search self.assertListEqual(list(dfs_nodes(self.graph, 8)), [8, 9, 12, 13, 28, 10, 11, 26, 27]) self.assertListEqual(list(dfs_nodes(self.graph, 8, method='bfs')), [8, 9, 10, 12, 11, 13, 26, 28, 27]) def test_algorithm_is_reachable(self): """ Test is_reachable method to test connectivity between two nodes """ self.assertTrue(is_reachable(self.graph, 3, 21)) # Reverse directionality of edge (20, 21) self.graph.add_edge(21, 20) self.graph.remove_edge(20, 21) self.assertFalse(is_reachable(self.graph, 7, 23)) def test_algorithm_brandes_betweenness_centrality(self): """ Test graph Brandes betweenness centrality measure """ # Non-weighted Brandes betweenness centrality self.assertDictEqual( brandes_betweenness_centrality(self.graph), { 1: 0.0, 2: 0.038461538461538464, 3: 0.026153846153846153, 4: 0.04461538461538461, 5: 0.047692307692307694, 7: 0.08461538461538462, 8: 0.03230769230769231, 9: 0.00923076923076923, 10: 0.016923076923076923, 11: 0.013846153846153847, 12: 0.023076923076923078, 13: 0.01846153846153846, 14: 0.023076923076923078, 15: 0.01846153846153846, 16: 0.0, 17: 0.06461538461538462, 18: 0.06461538461538462, 19: 0.0, 20: 0.04923076923076923, 21: 0.04153846153846154, 22: 0.03076923076923077, 23: 0.0, 24: 0.0, 25: 0.01846153846153846, 26: 0.015384615384615385, 27: 0.0, 28: 0.0 }) # Weighted Brandes betweenness centrality self.assertDictEqual( brandes_betweenness_centrality(self.graph, weight='weight'), { 1: 0.0, 2: 0.038461538461538464, 3: 0.021538461538461538, 4: 0.04923076923076923, 5: 0.06153846153846154, 7: 0.08461538461538462, 8: 0.046153846153846156, 9: 0.027692307692307693, 10: 0.012307692307692308, 11: 0.00923076923076923, 12: 0.027692307692307693, 13: 0.01846153846153846, 14: 0.00923076923076923, 15: 0.004615384615384615, 16: 0.0, 17: 0.06461538461538462, 18: 0.06461538461538462, 19: 0.0, 20: 0.04923076923076923, 21: 0.04153846153846154, 22: 0.03076923076923077, 23: 0.0, 24: 0.0, 25: 0.01846153846153846, 26: 0.015384615384615385, 27: 0.0, 28: 0.0 }) # Non-Normalized Brandes betweenness centrality self.assertDictEqual( brandes_betweenness_centrality(self.graph, normalized=False), { 1: 0.0, 2: 25.0, 3: 17.0, 4: 29.0, 5: 31.0, 7: 55.0, 8: 21.0, 9: 6.0, 10: 11.0, 11: 9.0, 12: 15.0, 13: 12.0, 14: 15.0, 15: 12.0, 16: 0.0, 17: 42.0, 18: 42.0, 19: 0.0, 20: 32.0, 21: 27.0, 22: 20.0, 23: 0.0, 24: 0.0, 25: 12.0, 26: 10.0, 27: 0.0, 28: 0.0 }) def test_algorithm_eigenvector_centrality(self): """ Test graph node eigenvector centrality """ # Default eigenvector centrality self.assertDictAlmostEqual(eigenvector_centrality(self.graph, max_iter=1000), { 1: 4.625586668162422e-22, 2: 2.585702947502789e-19, 3: 7.21415747939946e-17, 4: 7.21415747939946e-17, 5: 2.6788916354749308e-14, 7: 3.737126037589252e-12, 8: 3.723731579643154e-12, 9: 4.133449353873311e-10, 10: 4.133449353873311e-10, 11: 3.816675918922932e-08, 12: 3.8373431692993267e-08, 13: 6.049668071167027e-06, 14: 1.3394458408653937e-14, 15: 1.861865919106721e-12, 16: 1.861865919106721e-12, 17: 4.152068010478676e-10, 18: 3.837343162085218e-08, 19: 3.0343757153351047e-06, 20: 3.0343757153351047e-06, 21: 0.0002095723846317974, 22: 0.012842890187130716, 23: 0.7070483707650801, 24: 0.7070483707650801, 25: 4.152068010478676e-10, 26: 3.0536657740585734e-06, 27: 0.00021109911510684646, 28: 0.0004176371258851023 }, places=14) # Weighted eigenvector centrality self.assertDictAlmostEqual(eigenvector_centrality(self.graph, max_iter=1000, weight='weight'), { 1: 8.688902566026301e-23, 2: 5.899764842331867e-20, 3: 2.0000289704530646e-17, 4: 2.0000289704530646e-17, 5: 9.026876112472413e-15, 7: 1.148684996586023e-12, 8: 1.5255620780686783e-12, 9: 1.029772476508652e-10, 10: 6.178634859047565e-10, 11: 2.0822457823635333e-07, 12: 6.607834043233963e-09, 13: 2.131713811356956e-05, 14: 9.026876112472416e-15, 15: 3.051124156050468e-12, 16: 1.5255620780686783e-12, 17: 1.5522865250051764e-10, 18: 1.745502542904305e-08, 19: 3.359775472482072e-06, 20: 1.679887736241036e-06, 21: 0.00014125541592609745, 22: 0.010542254842300024, 23: 0.7070653405331172, 24: 0.7070653405331172, 25: 1.5522865250051764e-10, 26: 2.0037291488250496e-05, 27: 0.0016833983234686718, 28: 0.0017929426478924984 }, places=14) # Non-convergence exception self.assertRaises(GraphitAlgorithmError, eigenvector_centrality, self.graph, max_iter=100)
def read_tgf(tgf, graph=None, key_tag=None): """ Read graph in Trivial Graph Format TGF format dictates that nodes to be listed in the file first with each node on a new line. A '#' character signals the end of the node list and the start of the edge list. Node and edge ID's can be integers, float or strings. They are parsed automatically to their most likely format. Simple node and edge labels are supported in TGF as all characters that follow the node or edge ID's. They are parsed and stored in the Graph node and edge data stores using the graphs default or custom 'key_tag'. TGF data is imported into a default Graph object if no custom Graph instance is provided. The graph behaviour and the data import process is influenced and can be controlled using a (custom) Graph class. .. note:: TGF format always defines edges in a directed fashion. This is enforced even for custom graphs. :param tgf: TGF graph data. :type tgf: File, string, stream or URL :param graph: Graph object to import TGF data in :type graph: :graphit:Graph :param key_tag: Data key to use for parsed node/edge labels. :type key_tag: :py:str :return: Graph object :rtype: :graphit:Graph """ tgf_file = open_anything(tgf) if not isinstance(graph, Graph): graph = Graph() # Define node/edge data labels if key_tag: graph.key_tag = key_tag # TGF defines edges in a directed fashion. Enforce but restore later default_directionality = graph.directed graph.directed = True # TGF node and edge labels are unique, turn off auto_nid graph.auto_nid = False # Start parsing. First extract nodes nodes = True node_dict = {} for line in tgf_file.readlines(): line = line.strip() if len(line): # Reading '#' character means switching from node # definition to edges if line.startswith('#'): nodes = False continue # Coarse string to types line = [coarse_type(n) for n in line.split()] # Parse nodes if nodes: attr = {} # Has node data if len(line) > 1: attr = {graph.key_tag: ' '.join(line[1:])} nid = graph.add_node(line[0], **attr) node_dict[line[0]] = nid # Parse edges else: e1 = node_dict[line[0]] e2 = node_dict[line[1]] attr = {} # Has edge data if len(line) > 2: attr = {graph.key_tag: ' '.join(line[2:])} graph.add_edge(e1, e2, **attr) tgf_file.close() # Restore directionality graph.directed = default_directionality return graph
class TestGraphEdgeAttribute(UnittestPythonCompatibility): """ Test methods to get and set edge attributes using an edge storage driver. `DictStorage` is the default driver tested here which happens to be the same as for nodes. """ def setUp(self): """ Build default Graph with node and edge attributes """ self.graph = Graph() self.graph.add_nodes([('g', {'weight': 1.0, 'value': 'gr'}), ('r', {'weight': 1.5, 'value': 'ra'}), ('a', {'weight': 2.0, 'value': 'ap'}), ('p', {'weight': 2.5, 'value': 'ph'}), ('h', {'weight': 3.0})]) self.graph.add_edges([(1, 2), (2, 3), (3, 4), (3, 5), (4, 5)], value=True, weight=43.2, key='edge') def test_graph_edge_attr_storeget(self): """ Test getting edge attributes directly from the `edges` storage """ self.assertEqual(self.graph.edges[(1, 2)]['value'], True) self.assertEqual(self.graph.edges[(1, 2)]['weight'], 43.2) def test_graph_edge_attr_storeset(self): """ Test setting node attributes directly from the `edges` storage """ self.graph.edges[(1, 2)]['weight'] = 5.0 self.graph.edges[(2, 3)]['value'] = 'dd' self.assertEqual(self.graph.edges[(1, 2)]['weight'], 5.0) self.assertEqual(self.graph.edges[(2, 3)]['value'], 'dd') def test_graph_edge_attr_key_tag(self): """ Test get attributes based on `key_tag` """ self.assertEqual(self.graph.edges[(1, 2)][self.graph.data.key_tag], 'edge') self.assertEqual(self.graph.get((1, 2)), 'edge') # uses default node data tag def test_graph_edge_attr_value_tag(self): """ Test get attributes based on `value_tag` """ self.assertEqual(self.graph.edges[(4, 5)][self.graph.data.value_tag], True) def test_graph_edge_attr_dict(self): """ Test if the returned full attribute dictionary is of expected format """ self.assertDictEqual(self.graph.edges[(4, 5)], {'value': True, 'weight': 43.2, 'key': 'edge'}) def test_graph_edge_attr_exception(self): """ Test `edges` exception if edge not present """ self.assertRaises(GraphitException, self.graph.__getitem__, (5, 6)) self.assertIsNone(self.graph.nodes.get((5, 6))) def test_graph_edge_attr_graphget(self): """ Test access edge attributes by nid using the (sub)graph 'get' method """ self.assertEqual(self.graph.get((1, 2)), 'edge') self.assertEqual(self.graph.get((1, 2), 'weight'), 43.2) # Key does not exist self.assertIsNone(self.graph.get((1, 2), key='no_key')) # Key does not exist return defaultkey self.assertEqual(self.graph.get((1, 2), key='no_key', defaultattr='weight'), 43.2) def test_graph_edge_attr_singleedge_set(self): """ Test setting edge attribute values directly using the single edge Graph API which has the required methods (edge_tools) added to it. """ edge = self.graph.getedges((1, 2), directed=True) edge.weight = 4.5 edge['key'] = 'edge_set' edge.set('value', False) self.assertEqual(edge.edges[(1, 2)]['weight'], 4.5) self.assertEqual(edge.edges[(1, 2)]['key'], 'edge_set') self.assertEqual(edge.edges[(1, 2)]['value'], False) def test_graph_edge_attr_singleedge_exception(self): """ Test exceptions in direct access to edge attributes in a single graph class """ edge = self.graph.getedges((1, 2), directed=True) self.assertEqual(edge.get(), True) self.assertRaises(KeyError, edge.__getitem__, 'no_key') self.assertRaises(AttributeError, edge.__getattr__, 'no_key') def test_graph_edge_attr_undirectional(self): """ Undirectional edge has one attribute store """ # True for DictStorage but may be different for other drivers self.assertEqual(id(self.graph.edges[(1, 2)]), id(self.graph.edges[(2, 1)])) self.assertDictEqual(self.graph.edges[(1, 2)], self.graph.edges[(2, 1)]) self.graph.edges[(1, 2)]['key'] = 'edge_modified' self.assertTrue(self.graph.edges[(2, 1)]['key'] == 'edge_modified') def test_graph_edge_attr_directional(self): """ Directional edge has two separated attribute stores """ self.graph.add_edge(2, 5, directed=True, attr='to') self.graph.add_edge(5, 2, directed=True, attr='from') # True for DictStorage but may be different for other drivers self.assertNotEqual(id(self.graph.edges[(2, 5)]), id(self.graph.edges[(5, 2)])) self.assertNotEqual(self.graph.edges[(2, 5)], self.graph.edges[(5, 2)]) self.graph.edges[(5, 2)]['attr'] = 'return' self.assertTrue(self.graph.edges[(2, 5)]['attr'] == 'to') self.assertTrue(self.graph.edges[(5, 2)]['attr'] == 'return')
class TestGraphMixed(UnittestPythonCompatibility): """ Test graph with mixed directed and undirected edges 1 - 2 - 3 - 6 | / | 4 - 5 """ def setUp(self): """ Build mixed directed and undirected Graph """ self.graph = Graph(directed=True) self.graph.add_edges([(1, 2), (2, 3), (3, 2), (3, 6), (3, 5), (5, 4), (4, 5), (4, 2)], node_from_edge=True, arg1=1.22, arg2=False) self.graph.add_edge(3, 4, directed=False, node_from_edge=True, arg1=1.22, arg2=False) for i, edge in enumerate(self.graph.iteredges()): edge['nd'] = i def test_graph_is_mixed(self): self.assertTrue(self.graph.directed) self.assertEqual(graph_directionality(self.graph), 'mixed') self.assertEqual( sum([1 for edge in self.graph.iteredges() if edge.is_directed]), 4) self.assertEqual( sum([1 for edge in self.graph.iteredges() if not edge.is_directed]), 6) self.assertTrue(len(self.graph.edges) == 10) def test_graph_contains(self): """ Test a mix of directed and undirected (pairs) edges """ directed = [edge.is_directed for edge in self.graph.iteredges()] self.assertEqual( directed, [True, False, False, False, True, True, True, False, False, False]) def test_graph_directional_to_undirectional(self): """ Test conversion of a directional to undirectional graph """ undirectional = graph_directional_to_undirectional(self.graph) # directed attribute changed to True self.assertFalse(undirectional.directed) self.assertNotEqual(undirectional, self.graph) # data reference pointers determine directionality self.assertEqual(graph_directionality(undirectional), 'undirectional') self.assertEqual( graph_directionality(undirectional, has_data_reference=False), 'undirectional') # all edges exist in pairs resulting duplicated values values = undirectional.edges(data='nd').values() self.assertEqual(len(values), len(set(values)) * 2)
class TestGraphAlgorithms(UnittestPythonCompatibility): def setUp(self): edges = { (5, 4): { 'type': 'universal' }, (5, 6): { 'type': 'universal' }, (11, 9): { 'type': 'universal' }, (3, 2): { 'type': 'universal' }, (2, 1): { 'type': 'monotone' }, (9, 10): { 'type': 'universal' }, (2, 3): { 'type': 'universal' }, (9, 6): { 'type': 'universal' }, (6, 5): { 'type': 'universal' }, (1, 2): { 'type': 'monotone' }, ('object', 12): { 'type': 'universal' }, (6, 9): { 'type': 'universal' }, (6, 7): { 'type': 'universal' }, (12, 13): { 'type': 'monotone' }, (7, 8): {}, (7, 6): { 'type': 'universal' }, (13, 12): { 'type': 'monotone' }, (3, 8): { 'type': 'universal' }, (4, 5): { 'type': 'universal' }, (12, 'object'): { 'type': 'universal' }, (9, 11): { 'type': 'universal' }, (4, 3): { 'type': 'universal' }, (8, 3): { 'type': 'universal' }, (3, 4): { 'type': 'universal' }, (10, 9): { 'type': 'universal' } } self.graph = Graph(auto_nid=False) self.graph.directed = True self.gn = NetworkXGraph() self.gn.directed = True self.nx = networkx.DiGraph() weight = 0 for node in range(1, 14): self.graph.add_node(node, weight=weight) self.gn.add_node(node, weight=weight) self.nx.add_node(node, _id=node, key=node, weight=weight) weight += 1 self.graph.add_node('object') self.gn.add_node('object') self.nx.add_node('object', _id=node + 1, key='object') weight = 0 for eid in sorted(edges.keys(), key=lambda x: str(x[0])): self.graph.add_edge(*eid, weight=weight) self.gn.add_edge(*eid, weight=weight) self.nx.add_edge(*eid, weight=weight) weight += 0.05 def test_graph_shortest_path_method(self): """ Test Dijkstra shortest path method """ from networkx.algorithms.shortest_paths.generic import shortest_path from networkx.algorithms.traversal.depth_first_search import dfs_preorder_nodes print(shortest_path(self.nx, 8, 10)) print(list(dfs_preorder_nodes(self.nx, 8))) # In a mixed directed graph where 7 connects to 8 but not 8 to 7 self.assertEqual(dijkstra_shortest_path(self.graph, 8, 10), [8, 3, 4, 5, 6, 9, 10]) self.assertEqual(list(dfs_paths(self.graph, 8, 10)), [[8, 3, 4, 5, 6, 9, 10]]) self.assertEqual(list(dfs_paths(self.graph, 8, 10, method='bfs')), [[8, 3, 4, 5, 6, 9, 10]]) # Fully connect 7 and 8 self.graph.add_edge(8, 7, directed=True) self.assertEqual(dijkstra_shortest_path(self.graph, 8, 10), [8, 7, 6, 9, 10]) self.assertEqual(list(dfs_paths(self.graph, 8, 10)), [[8, 7, 6, 9, 10], [8, 3, 4, 5, 6, 9, 10]]) self.assertEqual(list(dfs_paths(self.graph, 8, 10, method='bfs')), [[8, 7, 6, 9, 10], [8, 3, 4, 5, 6, 9, 10]]) def test_graph_dfs_method(self): """ Test graph depth-first-search and breath-first-search """ # Connectivity information using Depth First Search / Breath first search self.assertListEqual(dfs(self.graph, 8), [8, 3, 4, 5, 6, 9, 11, 10, 7, 2, 1]) self.assertListEqual(dfs(self.graph, 8, method='bfs'), [8, 3, 2, 4, 1, 5, 6, 7, 9, 10, 11]) def test_graph_node_reachability_methods(self): """ Test graph algorithms """ # Test if node is reachable from other node (uses dfs internally) self.assertTrue(is_reachable(self.graph, 8, 10)) self.assertFalse(is_reachable(self.graph, 8, 12)) def test_graph_centrality_method(self): """ Test graph Brandes betweenness centrality measure """ # Return Brandes betweenness centrality self.assertDictEqual( brandes_betweenness_centrality(self.graph), { 1: 0.0, 2: 0.11538461538461538, 3: 0.26282051282051283, 4: 0.21474358974358973, 5: 0.22756410256410256, 6: 0.3205128205128205, 7: 0.0673076923076923, 8: 0.060897435897435896, 9: 0.21794871794871795, 10: 0.0, 11: 0.0, 12: 0.01282051282051282, 13: 0.0, u'object': 0.0 }) print(brandes_betweenness_centrality(self.graph, weight='weight')) print(brandes_betweenness_centrality(self.graph, normalized=False)) # Test against NetworkX if possible if self.nx is not None: from networkx.algorithms.centrality.betweenness import betweenness_centrality # Regular Brandes betweenness centrality nx_between = betweenness_centrality(self.nx) gn_between = brandes_betweenness_centrality(self.graph) self.assertDictEqual(gn_between, nx_between) # Weighted Brandes betweenness centrality nx_between = betweenness_centrality(self.nx, weight='weight') gn_between = brandes_betweenness_centrality(self.graph, weight='weight') self.assertDictEqual(gn_between, nx_between) # Normalized Brandes betweenness centrality nx_between = betweenness_centrality(self.nx, normalized=False) gn_between = brandes_betweenness_centrality(self.graph, normalized=False) self.assertDictEqual(gn_between, nx_between) def test_graph_nodes_are_interconnected(self): """ Test if all nodes directly connected with one another """ nodes = [1, 2, 3, 4, 5, 6] self.graph = Graph() self.graph.add_nodes(nodes) for edge in itertools.combinations(nodes, 2): self.graph.add_edge(*edge) self.graph.remove_edge(5, 6) self.assertTrue(nodes_are_interconnected(self.graph, [1, 2, 4])) self.assertFalse(nodes_are_interconnected(self.graph, [3, 5, 6])) def test_graph_degree(self): """ Test (weighted) degree method """ self.assertDictEqual(degree(self.graph, [1, 3, 12]), { 1: 1, 3: 3, 12: 2 }) # Directed graphs behave the same as undirected self.graph.directed = False self.assertDictEqual(degree(self.graph, [1, 3, 12]), { 1: 1, 3: 3, 12: 2 }) self.assertDictEqual(degree(self.graph, [1, 3, 12], weight='weight'), { 1: 0, 3: 1.3499999999999999, 12: 0.35000000000000003 }) # Loops counted twice self.graph.add_edge(12, 12) self.assertDictEqual(degree(self.graph, [1, 3, 12]), { 1: 1, 3: 3, 12: 4 }) self.assertDictEqual(degree(self.graph, [1, 3, 12], weight='weight'), { 1: 0, 3: 1.3499999999999999, 12: 2.3499999999999996 })
def read_p2g(p2g_file, graph=None): """ Read graph in P2G format :param p2g_file: P2G data to parse :type p2g_file: File, string, stream or URL :param graph: Graph object to import to or Graph by default :type graph: :graphit:Graph :return: Graph instance :rtype: :graphit:Graph """ p2g_file = open_anything(p2g_file) if graph is None: graph = Graph() elif not isinstance(graph, Graph): raise GraphitException('Unsupported graph type {0}'.format( type(graph))) # P2G graphs are directed graph.directed = True graph_name = None graph_layout = None curr_node = None nodes = {} for i, line in enumerate(p2g_file.readlines()): line = line.strip() if line: # Parse p2g graph name (first line) sline = line.split() if not graph_name: graph_name = line continue # Parse number of nodes and edges (second line) elif not graph_layout: try: graph_layout = map(int, sline) except ValueError: raise GraphitException( 'P2G import error: line {0} - {1}'.format(i, line)) continue # Parse nodes and edges if len(sline) == 1: nodes[line] = [] curr_node = line elif len(sline) == 2: try: nodes[curr_node] = map(int, sline) except ValueError: raise GraphitException( 'P2G import error: malformed edge on line {0} - {1}'. format(i, line)) else: raise GraphitException( 'P2G import error: line {0} - {1}'.format(i, line)) graph.data['name'] = graph_name # Add nodes mapped_nodes = graph.add_nodes(nodes.keys()) # Add edges for i, nid in enumerate(nodes.keys()): for e in nodes[nid]: if e < len(mapped_nodes): graph.add_edge(mapped_nodes[i], mapped_nodes[e]) else: raise GraphitException( 'P2G import error: edge node index {0} not in graph'. format(e)) if len(nodes) != graph_layout[0] or (len(graph.edges)) != graph_layout[1]: logging.warning( 'P2G import warning: declared number of nodes and edges {0}-{1} does not match {2}-{3}' .format(graph_layout[0], graph_layout[1], len(nodes), len(graph.edges))) return graph
def read_lgr(lgr, graph=None, edge_label='label'): """ Read graph in LEDA format Nodes are added to the graph using a unique ID or with the node data as label depending if the graph.data.auto_nid is True or False. Edge data is added to the edge attributes using `edge_label` as key. The data types for both nodes and edges is set according to the specifications in the LEDA header as either string, int, float or bool. :param lgr: LEDA graph data. :type lgr: File, string, stream or URL :param graph: Graph object to import LEDA data in :type graph: :graphit:Graph :param edge_label: edge data label name :type edge_label: :py:str :return: Graph object :rtype: :graphit:Graph :raises: TypeError if node/edge type conversion failed GraphitException in case of malformed LEDA file """ # User defined or default Graph object if graph is None: graph = Graph() elif not isinstance(graph, Graph): raise GraphitException('Unsupported graph type {0}'.format( type(graph))) # Parse LEDA file lgr_file = open_anything(lgr) header = [] nodes = [] edges = [] container = header for line in lgr_file.readlines(): line = line.strip() if line: if line.startswith('#header'): container = header continue if line.startswith('#nodes'): container = nodes continue if line.startswith('#edges'): container = edges continue container.append(line) # Parse LEDA header if not header[0] == 'LEDA.GRAPH': raise GraphitException('File is not a valid LEDA graph format') # Node and edge data types and graph directionality node_type = data_types.get(header[1]) edge_type = data_types.get(header[2]) graph.directed = int(header[3]) == -1 # Parse LEDA nodes node_mapping = {} for i, node in enumerate(nodes[1:], start=1): data = node.strip('|{}|') or None if node_type and data: data = node_type(data) nid = graph.add_node(data) node_mapping[i] = nid # Parse LEDA edges for edge in edges[1:]: try: source, target, reversal, label = edge.split() except ValueError: raise GraphitException( 'Too few fields in LEDA edge {0}'.format(edge)) attr = {edge_label: label.strip('|{}|') or None} if edge_type and attr[edge_label]: attr[edge_label] = edge_type(attr[edge_label]) graph.add_edge(node_mapping[int(source)], node_mapping[int(target)], **attr) return graph
class TestGraphAddNodeConnected(UnittestPythonCompatibility): """ Test add_connect method for direct addition and edge connection of a new node to an existing node using a single node object """ currpath = os.path.dirname(__file__) def setUp(self): """ Build empty graph to add a node to and test default state """ self.graph = Graph(auto_nid=False) # Add two nodes self.graph.add_nodes(('one', 'two')) self.graph.add_edge('one', 'two') # Two nodes and one edge self.assertTrue(len(self.graph) == 2) self.assertTrue(len(self.graph.nodes) == 2) self.assertTrue(len(self.graph.edges) == 2) self.assertTrue(len(self.graph.adjacency) == 2) # auto_nid self.assertFalse(self.graph.data.auto_nid) def tearDown(self): """ Test state after node addition """ nids = sorted(self.graph.nodes) # The nid should equal the node self.assertTrue(self.node in nids) # The _id is still set self.assertEqual(self.graph.nodes[self.node]['_id'], 3) self.assertEqual(self.graph.data.nodeid, 4) # filled after addition self.assertTrue(len(self.graph) == 3) self.assertTrue(len(self.graph.nodes) == 3) self.assertTrue(len(self.graph.edges) == 4) self.assertTrue(len(self.graph.adjacency) == 3) # no adjacency self.assertTrue(len(self.graph.adjacency[nids[0]]) == 1) def test_add_connect_string(self): """ Test add connect a string node """ self.node = 'three' node = self.graph.getnodes('two') node.add_connect(self.node) self.assertTrue(('two', 'three') in self.graph.edges) self.assertTrue(('three', 'two') in self.graph.edges) def test_add_connect_attr(self): """ Test add connect a node with node and edge attributes """ self.node = 'three' node = self.graph.getnodes('two') node.add_connect(self.node, node_kwargs={ 'arg': True, 'n': 1.22 }, edge_kwargs={ 'arg': True, 'e': 5.44 }) # Node should contain keyword arguments self.assertTrue(self.graph.nodes[self.node]['arg']) self.assertTrue(self.graph.nodes[self.node]['n'] == 1.22) # Edges should contain keyword arguments edge = self.graph.getedges(('two', self.node)) self.assertTrue(all(e.get('arg', False) for e in edge.edges.values())) self.assertTrue(all(e.get('e') == 5.44 for e in edge.edges.values()))
def read_gexf(gexf_file, graph=None): """ Read graphs in GEXF format Uses the Python build-in etree cElementTree parser to parse the XML document and convert the elements into nodes. The XML element tag becomes the node key, XML text becomes the node value and XML attributes are added to the node as additional attributes. :param gexf_file: XML data to parse :type gexf_file: File, string, stream or URL :param graph: Graph object to import dictionary data in :type graph: :graphit:Graph :return: GraphAxis object :rtype: :graphit:GraphAxis """ gexf_file = open_anything(gexf_file) # User defined or default Graph object if graph is None: graph = Graph() elif not isinstance(graph, Graph): raise GraphitException('Unsupported graph type {0}'.format( type(graph))) # Try parsing the string using default Python cElementTree parser try: tree = et.fromstring(gexf_file.read()) except et.ParseError as error: logging.error( 'Unable to parse GEXF file. cElementTree error: {0}'.format(error)) return # Get XMLNS namespace from root xmlns = None for elem in tree.iter(): if elem.tag.endswith('gexf'): xmlns = elem.tag.split('}')[0] + '}' break if xmlns is None: raise GraphitException( 'Invalid GEXF file format, "gexf" tag not found') # Add graph meta-data and XMLNS namespace for meta in tree.iter('{0}meta'.format(xmlns)): graph.data.update(meta.attrib) for meta_data in meta: tag = meta_data.tag.split('}')[1] graph.data[tag] = meta_data.text # GEXF node and edge labels are unique, turn off auto_nid graph.data['auto_nid'] = False graph_tag = tree.find('{0}graph'.format(xmlns)) graph.directed = graph_tag.get('defaultedgetype', 'directed') == 'directed' graph.data.update(graph_tag.attrib) # Parse all nodes nodes = tree.findall('.//{0}node'.format(xmlns)) if not len(nodes): raise GraphitException('GEXF file containes no "node" elements') for node in nodes: attr = node.attrib attr = parse_attvalue_elements(node, attr, xmlns=xmlns) graph.add_node(attr['id'], **dict([n for n in attr.items() if n[0] != 'id'])) # Parse all edges edges = tree.findall('.//{0}edge'.format(xmlns)) for edge in edges: attr = edge.attrib # Edge direction differs from global graph directionality edge_directed = graph.directed if 'type' in attr: edge_directed = attr['type'] == 'directed' attr = parse_attvalue_elements(edge, attr, xmlns=xmlns) graph.add_edge(attr['source'], attr['target'], directed=edge_directed, **dict([ n for n in attr.items() if n[0] not in ('source', 'target') ])) logger.info('Import graph in GEXF format. XMLNS: {0}'.format(xmlns)) return graph