class TestGraphAddNodeAttributes(UnittestPythonCompatibility): """ Test additional attribute storage for Graph add_node """ def setUp(self): """ Build empty graph to add a node to and test default state """ self.graph = Graph() def tearDown(self): """ Test state after node addition """ self.attr.update({'_id': 1, self.graph.key_tag: 10}) self.assertDictEqual(self.graph.nodes[1], self.attr) def test_add_node_no_attribute(self): """ No attributes added should yield the default '_id' and 'key' """ self.attr = {} self.graph.add_node(10, **self.attr) def test_add_node_single_attribute(self): """ Add a single attribute """ self.attr = {'weight': 2.33} self.graph.add_node(10, **self.attr) def test_add_node_multiple_attribute(self): """ Add a multiple attributes """ self.attr = {'test': True, 'pv': 1.44} self.graph.add_node(10, **self.attr) def test_add_node_protected_attribute(self): """ The '_id' attribute is protected """ self.attr = {} self.graph.add_node(10, _id=5) def test_add_node_nested_attribute(self): """ Test adding nested attributed, e.a. dict in dict """ self.attr = {'func': len, 'nested': {'weight': 1.22, 'leaf': True}} self.graph.add_node(10, **self.attr)
class TestGraphAddNodeExceptionWarning(UnittestPythonCompatibility): """ Test logged warnings and raised Exceptions by Graph add_node. 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_node_none(self): """ Unable to add 'None' node when auto_nid False """ # no problem when auto_nid self.graph.add_node() self.assertTrue(len(self.graph) == 1) self.graph.auto_nid = False self.assertRaises(GraphitException, self.graph.add_node, None) def test_add_node_hasable(self): """ When auto_nid equals False, the nid should be a hashable object :return: """ self.graph.auto_nid = False self.assertRaises(GraphitException, self.graph.add_node, [1, 2]) def test_add_node_duplicate(self): """ Duplication is no problem with auto_nid but without the previous node is updated with the attributes of the new one. A warning is logged. """ # With auto_nid self.graph.add_nodes([1, 1]) self.assertEqual(len(self.graph), 2) # Without auto_nid self.graph.auto_nid = False self.graph.add_nodes([3, 3]) self.assertEqual(len(self.graph), 3) self.assertItemsEqual(self.graph.keys(), [1, 1, 3]) self.assertItemsEqual(self.graph.keys('_id'), [1, 2, 3])
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 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_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 TestGraphAddNode(UnittestPythonCompatibility): """ Test Graph add_node method with the Graph.auto_nid set to False mimicking the behaviour of many popular graph packages """ currpath = os.path.dirname(__file__) image = os.path.join(currpath, '../', 'files', 'graph_tgf.png') def setUp(self): """ Build empty graph to add a node to and test default state """ self.graph = Graph(auto_nid=False) # empty before addition self.assertTrue(len(self.graph) == 0) self.assertTrue(len(self.graph.nodes) == 0) self.assertTrue(len(self.graph.edges) == 0) self.assertTrue(len(self.graph.adjacency) == 0) # auto_nid self.assertFalse(self.graph.auto_nid) def tearDown(self): """ Test state after node addition """ nid = list(self.graph.nodes) # The nid should equal the node self.assertEqual(nid, [self.node]) # The _id is still set self.assertEqual(self.graph.nodes[self.node]['_id'], 1) self.assertEqual(self.graph._nodeid, 2) # filled after addition self.assertTrue(len(self.graph) == 1) self.assertTrue(len(self.graph.nodes) == 1) self.assertTrue(len(self.graph.edges) == 0) self.assertTrue(len(self.graph.adjacency) == 1) # no adjacency self.assertTrue(len(self.graph.adjacency[nid[0]]) == 0) # node key self.assertItemsEqual(self.graph.keys(), [self.node]) def test_add_node_string(self): """ Test adding a single node, string type """ self.node = 'first' nid = self.graph.add_node(self.node) # Added string should be unicode self.assertIsInstance(self.graph.nodes[nid][self.graph.key_tag], UNICODE_TYPE) def test_add_node_int(self): """ Test adding a single node, int type """ self.node = 100 self.graph.add_node(self.node) def test_add_node_float(self): """ Test adding a single node, float type """ self.node = 4.55 self.graph.add_node(self.node) def test_add_node_bool(self): """ Test adding a single node, float bool """ self.node = False self.graph.add_node(self.node) def test_add_node_function(self): """ Test adding a single node, function type """ self.node = map self.graph.add_node(self.node) def test_add_node_object(self): """ Test adding an object as a single node. In this case the object is file """ self.node = open(self.image, 'r') self.graph.add_node(self.node)
class TestGraphAddNodeAutonid(UnittestPythonCompatibility): """ Test Graph add_node method using different input with the Graph class set to default auto_nid = True """ currpath = os.path.dirname(__file__) image = os.path.join(currpath, '../', 'files', 'graph_tgf.png') def setUp(self): """ Build empty graph to add a node to and test default state """ self.graph = Graph() # empty before addition self.assertTrue(len(self.graph) == 0) self.assertTrue(len(self.graph.nodes) == 0) self.assertTrue(len(self.graph.edges) == 0) self.assertTrue(len(self.graph.adjacency) == 0) # auto_nid self.assertTrue(self.graph.auto_nid) self.assertEqual(self.graph._nodeid, 1) def tearDown(self): """ Test state after node addition """ nid = list(self.graph.nodes) # auto_nid self.assertItemsEqual(nid, [1]) self.assertEqual(self.graph._nodeid, 2) # filled after addition self.assertTrue(len(self.graph) == 1) self.assertTrue(len(self.graph.nodes) == 1) self.assertTrue(len(self.graph.edges) == 0) self.assertTrue(len(self.graph.adjacency) == 1) # no adjacency self.assertTrue(len(self.graph.adjacency[nid[0]]) == 0) # node key self.assertItemsEqual(self.graph.keys(), [self.node]) def test_add_node_string(self): """ Test adding a single node, string type """ self.node = 'first' nid = self.graph.add_node(self.node) # Added string should be unicode self.assertIsInstance(self.graph.nodes[nid][self.graph.key_tag], UNICODE_TYPE) def test_add_node_int(self): """ Test adding a single node, int type """ self.node = 100 self.graph.add_node(self.node) def test_add_node_float(self): """ Test adding a single node, float type """ self.node = 4.55 self.graph.add_node(self.node) def test_add_node_bool(self): """ Test adding a single node, float bool """ self.node = False self.graph.add_node(self.node) def test_add_node_function(self): """ Test adding a single node, function type """ self.node = map self.graph.add_node(self.node) def test_add_node_list(self): """ Test adding a single node, list type """ self.node = [1.22, 4.5, 6] self.graph.add_node(self.node) def test_add_node_set(self): """ Test adding a single node, set type """ self.node = {1.22, 4.5, 6} self.graph.add_node(self.node) def test_add_node_object(self): """ Test adding an object as a single node. In this case the object is file """ self.node = open(self.image, 'rb') self.graph.add_node(self.node) def test_add_node_image(self): """ Test adding an object as a single node. In this case the object is file and we do not convert it to unicode """ self.node = open(self.image, 'rb').read() self.graph.add_node(self.node, unicode_convert=False)
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