"""Functions for generating line graphs.""" from itertools import combinations from collections import defaultdict from functools import partial import networkx as nx from networkx.utils import arbitrary_element from networkx.utils.decorators import not_implemented_for __all__ = ["line_graph", "inverse_line_graph"] def line_graph(G, create_using=None): r"""Returns the line graph of the graph or digraph `G`. The line graph of a graph `G` has a node for each edge in `G` and an edge joining those nodes if the two edges in `G` share a common node. For directed graphs, nodes are adjacent exactly when the edges they represent form a directed path of length two. The nodes of the line graph are 2-tuples of nodes in the original graph (or 3-tuples for multigraphs, with the key of the edge as the third element). For information about self-loops and more discussion, see the **Notes** section below. Parameters ---------- G : graph A NetworkX Graph, DiGraph, MultiGraph, or MultiDigraph. create_using : NetworkX graph constructor, optional (default=nx.Graph) Graph type to create. If graph instance, then cleared before populated. Returns ------- L : graph The line graph of G. Examples -------- >>> G = nx.star_graph(3) >>> L = nx.line_graph(G) >>> print(sorted(map(sorted, L.edges()))) # makes a 3-clique, K3 [[(0, 1), (0, 2)], [(0, 1), (0, 3)], [(0, 2), (0, 3)]] Notes ----- Graph, node, and edge data are not propagated to the new graph. For undirected graphs, the nodes in G must be sortable, otherwise the constructed line graph may not be correct. *Self-loops in undirected graphs* For an undirected graph `G` without multiple edges, each edge can be written as a set `\{u, v\}`. Its line graph `L` has the edges of `G` as its nodes. If `x` and `y` are two nodes in `L`, then `\{x, y\}` is an edge in `L` if and only if the intersection of `x` and `y` is nonempty. Thus, the set of all edges is determined by the set of all pairwise intersections of edges in `G`. Trivially, every edge in G would have a nonzero intersection with itself, and so every node in `L` should have a self-loop. This is not so interesting, and the original context of line graphs was with simple graphs, which had no self-loops or multiple edges. The line graph was also meant to be a simple graph and thus, self-loops in `L` are not part of the standard definition of a line graph. In a pairwise intersection matrix, this is analogous to excluding the diagonal entries from the line graph definition. Self-loops and multiple edges in `G` add nodes to `L` in a natural way, and do not require any fundamental changes to the definition. It might be argued that the self-loops we excluded before should now be included. However, the self-loops are still "trivial" in some sense and thus, are usually excluded. *Self-loops in directed graphs* For a directed graph `G` without multiple edges, each edge can be written as a tuple `(u, v)`. Its line graph `L` has the edges of `G` as its nodes. If `x` and `y` are two nodes in `L`, then `(x, y)` is an edge in `L` if and only if the tail of `x` matches the head of `y`, for example, if `x = (a, b)` and `y = (b, c)` for some vertices `a`, `b`, and `c` in `G`. Due to the directed nature of the edges, it is no longer the case that every edge in `G` should have a self-loop in `L`. Now, the only time self-loops arise is if a node in `G` itself has a self-loop. So such self-loops are no longer "trivial" but instead, represent essential features of the topology of `G`. For this reason, the historical development of line digraphs is such that self-loops are included. When the graph `G` has multiple edges, once again only superficial changes are required to the definition. References ---------- * Harary, Frank, and Norman, Robert Z., "Some properties of line digraphs", Rend. Circ. Mat. Palermo, II. Ser. 9 (1960), 161--168. * Hemminger, R. L.; Beineke, L. W. (1978), "Line graphs and line digraphs", in Beineke, L. W.; Wilson, R. J., Selected Topics in Graph Theory, Academic Press Inc., pp. 271--305. """ if G.is_directed(): L = _lg_directed(G, create_using=create_using) else: L = _lg_undirected(G, selfloops=False, create_using=create_using) return L def _lg_directed(G, create_using=None): """Returns the line graph L of the (multi)digraph G. Edges in G appear as nodes in L, represented as tuples of the form (u,v) or (u,v,key) if G is a multidigraph. A node in L corresponding to the edge (u,v) is connected to every node corresponding to an edge (v,w). Parameters ---------- G : digraph A directed graph or directed multigraph. create_using : NetworkX graph constructor, optional Graph type to create. If graph instance, then cleared before populated. Default is to use the same graph class as `G`. """ L = nx.empty_graph(0, create_using, default=G.__class__) # Create a graph specific edge function. get_edges = partial(G.edges, keys=True) if G.is_multigraph() else G.edges for from_node in get_edges(): # from_node is: (u,v) or (u,v,key) L.add_node(from_node) for to_node in get_edges(from_node[1]): L.add_edge(from_node, to_node) return L def _lg_undirected(G, selfloops=False, create_using=None): """Returns the line graph L of the (multi)graph G. Edges in G appear as nodes in L, represented as sorted tuples of the form (u,v), or (u,v,key) if G is a multigraph. A node in L corresponding to the edge {u,v} is connected to every node corresponding to an edge that involves u or v. Parameters ---------- G : graph An undirected graph or multigraph. selfloops : bool If `True`, then self-loops are included in the line graph. If `False`, they are excluded. create_using : NetworkX graph constructor, optional (default=nx.Graph) Graph type to create. If graph instance, then cleared before populated. Notes ----- The standard algorithm for line graphs of undirected graphs does not produce self-loops. """ L = nx.empty_graph(0, create_using, default=G.__class__) # Graph specific functions for edges. get_edges = partial(G.edges, keys=True) if G.is_multigraph() else G.edges # Determine if we include self-loops or not. shift = 0 if selfloops else 1 # Introduce numbering of nodes node_index = {n: i for i, n in enumerate(G)} # Lift canonical representation of nodes to edges in line graph edge_key_function = lambda edge: (node_index[edge[0]], node_index[edge[1]]) edges = set() for u in G: # Label nodes as a sorted tuple of nodes in original graph. # Decide on representation of {u, v} as (u, v) or (v, u) depending on node_index. # -> This ensures a canonical representation and avoids comparing values of different types. nodes = [tuple(sorted(x[:2], key=node_index.get)) + x[2:] for x in get_edges(u)] if len(nodes) == 1: # Then the edge will be an isolated node in L. L.add_node(nodes[0]) # Add a clique of `nodes` to graph. To prevent double adding edges, # especially important for multigraphs, we store the edges in # canonical form in a set. for i, a in enumerate(nodes): edges.update( [ tuple(sorted((a, b), key=edge_key_function)) for b in nodes[i + shift :] ] ) L.add_edges_from(edges) return L @not_implemented_for("directed") @not_implemented_for("multigraph") def inverse_line_graph(G): """Returns the inverse line graph of graph G. If H is a graph, and G is the line graph of H, such that G = L(H). Then H is the inverse line graph of G. Not all graphs are line graphs and these do not have an inverse line graph. In these cases this function raises a NetworkXError. Parameters ---------- G : graph A NetworkX Graph Returns ------- H : graph The inverse line graph of G. Raises ------ NetworkXNotImplemented If G is directed or a multigraph NetworkXError If G is not a line graph Notes ----- This is an implementation of the Roussopoulos algorithm. If G consists of multiple components, then the algorithm doesn't work. You should invert every component seperately: >>> K5 = nx.complete_graph(5) >>> P4 = nx.Graph([("a", "b"), ("b", "c"), ("c", "d")]) >>> G = nx.union(K5, P4) >>> root_graphs = [] >>> for comp in nx.connected_components(G): ... root_graphs.append(nx.inverse_line_graph(G.subgraph(comp))) >>> len(root_graphs) 2 References ---------- * Roussopolous, N, "A max {m, n} algorithm for determining the graph H from its line graph G", Information Processing Letters 2, (1973), 108--112. """ if G.number_of_nodes() == 0: return nx.empty_graph(1) elif G.number_of_nodes() == 1: v = arbitrary_element(G) a = (v, 0) b = (v, 1) H = nx.Graph([(a, b)]) return H elif G.number_of_nodes() > 1 and G.number_of_edges() == 0: msg = ( "inverse_line_graph() doesn't work on an edgeless graph. " "Please use this function on each component seperately." ) raise nx.NetworkXError(msg) starting_cell = _select_starting_cell(G) P = _find_partition(G, starting_cell) # count how many times each vertex appears in the partition set P_count = {u: 0 for u in G.nodes} for p in P: for u in p: P_count[u] += 1 if max(P_count.values()) > 2: msg = "G is not a line graph (vertex found in more than two partition cells)" raise nx.NetworkXError(msg) W = tuple((u,) for u in P_count if P_count[u] == 1) H = nx.Graph() H.add_nodes_from(P) H.add_nodes_from(W) for a, b in combinations(H.nodes, 2): if any(a_bit in b for a_bit in a): H.add_edge(a, b) return H def _triangles(G, e): """Return list of all triangles containing edge e""" u, v = e if u not in G: raise nx.NetworkXError(f"Vertex {u} not in graph") if v not in G[u]: raise nx.NetworkXError(f"Edge ({u}, {v}) not in graph") triangle_list = [] for x in G[u]: if x in G[v]: triangle_list.append((u, v, x)) return triangle_list def _odd_triangle(G, T): """Test whether T is an odd triangle in G Parameters ---------- G : NetworkX Graph T : 3-tuple of vertices forming triangle in G Returns ------- True is T is an odd triangle False otherwise Raises ------ NetworkXError T is not a triangle in G Notes ----- An odd triangle is one in which there exists another vertex in G which is adjacent to either exactly one or exactly all three of the vertices in the triangle. """ for u in T: if u not in G.nodes(): raise nx.NetworkXError(f"Vertex {u} not in graph") for e in list(combinations(T, 2)): if e[0] not in G[e[1]]: raise nx.NetworkXError(f"Edge ({e[0]}, {e[1]}) not in graph") T_neighbors = defaultdict(int) for t in T: for v in G[t]: if v not in T: T_neighbors[v] += 1 for v in T_neighbors: if T_neighbors[v] in [1, 3]: return True return False def _find_partition(G, starting_cell): """Find a partition of the vertices of G into cells of complete graphs Parameters ---------- G : NetworkX Graph starting_cell : tuple of vertices in G which form a cell Returns ------- List of tuples of vertices of G Raises ------ NetworkXError If a cell is not a complete subgraph then G is not a line graph """ G_partition = G.copy() P = [starting_cell] # partition set G_partition.remove_edges_from(list(combinations(starting_cell, 2))) # keep list of partitioned nodes which might have an edge in G_partition partitioned_vertices = list(starting_cell) while G_partition.number_of_edges() > 0: # there are still edges left and so more cells to be made u = partitioned_vertices[-1] deg_u = len(G_partition[u]) if deg_u == 0: # if u has no edges left in G_partition then we have found # all of its cells so we do not need to keep looking partitioned_vertices.pop() else: # if u still has edges then we need to find its other cell # this other cell must be a complete subgraph or else G is # not a line graph new_cell = [u] + list(G_partition[u]) for u in new_cell: for v in new_cell: if (u != v) and (v not in G_partition[u]): msg = ( "G is not a line graph" "(partition cell not a complete subgraph)" ) raise nx.NetworkXError(msg) P.append(tuple(new_cell)) G_partition.remove_edges_from(list(combinations(new_cell, 2))) partitioned_vertices += new_cell return P def _select_starting_cell(G, starting_edge=None): """Select a cell to initiate _find_partition Parameters ---------- G : NetworkX Graph starting_edge: an edge to build the starting cell from Returns ------- Tuple of vertices in G Raises ------ NetworkXError If it is determined that G is not a line graph Notes ----- If starting edge not specified then pick an arbitrary edge - doesn't matter which. However, this function may call itself requiring a specific starting edge. Note that the r, s notation for counting triangles is the same as in the Roussopoulos paper cited above. """ if starting_edge is None: e = arbitrary_element(G.edges()) else: e = starting_edge if e[1] not in G[e[0]]: msg = f"starting_edge ({e[0]}, {e[1]}) is not in the Graph" raise nx.NetworkXError(msg) e_triangles = _triangles(G, e) r = len(e_triangles) if r == 0: # there are no triangles containing e, so the starting cell is just e starting_cell = e elif r == 1: # there is exactly one triangle, T, containing e. If other 2 edges # of T belong only to this triangle then T is starting cell T = e_triangles[0] a, b, c = T # ab was original edge so check the other 2 edges ac_edges = [x for x in _triangles(G, (a, c))] bc_edges = [x for x in _triangles(G, (b, c))] if len(ac_edges) == 1: if len(bc_edges) == 1: starting_cell = T else: return _select_starting_cell(G, starting_edge=(b, c)) else: return _select_starting_cell(G, starting_edge=(a, c)) else: # r >= 2 so we need to count the number of odd triangles, s s = 0 odd_triangles = [] for T in e_triangles: if _odd_triangle(G, T): s += 1 odd_triangles.append(T) if r == 2 and s == 0: # in this case either triangle works, so just use T starting_cell = T elif r - 1 <= s <= r: # check if odd triangles containing e form complete subgraph # there must be exactly s+2 of them # and they must all be connected triangle_nodes = set() for T in odd_triangles: for x in T: triangle_nodes.add(x) if len(triangle_nodes) == s + 2: for u in triangle_nodes: for v in triangle_nodes: if u != v and (v not in G[u]): msg = ( "G is not a line graph (odd triangles " "do not form complete subgraph)" ) raise nx.NetworkXError(msg) # otherwise then we can use this as the starting cell starting_cell = tuple(triangle_nodes) else: msg = ( "G is not a line graph (odd triangles " "do not form complete subgraph)" ) raise nx.NetworkXError(msg) else: msg = ( "G is not a line graph (incorrect number of " "odd triangles around starting edge)" ) raise nx.NetworkXError(msg) return starting_cell