1"""Functions for generating line graphs."""
2
3from collections import defaultdict
4from functools import partial
5from itertools import combinations
6
7import networkx as nx
8from networkx.utils import arbitrary_element
9from networkx.utils.decorators import not_implemented_for
10
11__all__ = ["line_graph", "inverse_line_graph"]
12
13
14@nx._dispatchable(returns_graph=True)
15def line_graph(G, create_using=None):
16 r"""Returns the line graph of the graph or digraph `G`.
17
18 The line graph of a graph `G` has a node for each edge in `G` and an
19 edge joining those nodes if the two edges in `G` share a common node. For
20 directed graphs, nodes are adjacent exactly when the edges they represent
21 form a directed path of length two.
22
23 The nodes of the line graph are 2-tuples of nodes in the original graph (or
24 3-tuples for multigraphs, with the key of the edge as the third element).
25
26 For information about self-loops and more discussion, see the **Notes**
27 section below.
28
29 Parameters
30 ----------
31 G : graph
32 A NetworkX Graph, DiGraph, MultiGraph, or MultiDigraph.
33 create_using : NetworkX graph constructor, optional (default=nx.Graph)
34 Graph type to create. If graph instance, then cleared before populated.
35
36 Returns
37 -------
38 L : graph
39 The line graph of G.
40
41 Examples
42 --------
43 >>> G = nx.star_graph(3)
44 >>> L = nx.line_graph(G)
45 >>> print(sorted(map(sorted, L.edges()))) # makes a 3-clique, K3
46 [[(0, 1), (0, 2)], [(0, 1), (0, 3)], [(0, 2), (0, 3)]]
47
48 Edge attributes from `G` are not copied over as node attributes in `L`, but
49 attributes can be copied manually:
50
51 >>> G = nx.path_graph(4)
52 >>> G.add_edges_from((u, v, {"tot": u + v}) for u, v in G.edges)
53 >>> G.edges(data=True)
54 EdgeDataView([(0, 1, {'tot': 1}), (1, 2, {'tot': 3}), (2, 3, {'tot': 5})])
55 >>> H = nx.line_graph(G)
56 >>> H.add_nodes_from((node, G.edges[node]) for node in H)
57 >>> H.nodes(data=True)
58 NodeDataView({(0, 1): {'tot': 1}, (2, 3): {'tot': 5}, (1, 2): {'tot': 3}})
59
60 Notes
61 -----
62 Graph, node, and edge data are not propagated to the new graph. For
63 undirected graphs, the nodes in G must be sortable, otherwise the
64 constructed line graph may not be correct.
65
66 *Self-loops in undirected graphs*
67
68 For an undirected graph `G` without multiple edges, each edge can be
69 written as a set `\{u, v\}`. Its line graph `L` has the edges of `G` as
70 its nodes. If `x` and `y` are two nodes in `L`, then `\{x, y\}` is an edge
71 in `L` if and only if the intersection of `x` and `y` is nonempty. Thus,
72 the set of all edges is determined by the set of all pairwise intersections
73 of edges in `G`.
74
75 Trivially, every edge in G would have a nonzero intersection with itself,
76 and so every node in `L` should have a self-loop. This is not so
77 interesting, and the original context of line graphs was with simple
78 graphs, which had no self-loops or multiple edges. The line graph was also
79 meant to be a simple graph and thus, self-loops in `L` are not part of the
80 standard definition of a line graph. In a pairwise intersection matrix,
81 this is analogous to excluding the diagonal entries from the line graph
82 definition.
83
84 Self-loops and multiple edges in `G` add nodes to `L` in a natural way, and
85 do not require any fundamental changes to the definition. It might be
86 argued that the self-loops we excluded before should now be included.
87 However, the self-loops are still "trivial" in some sense and thus, are
88 usually excluded.
89
90 *Self-loops in directed graphs*
91
92 For a directed graph `G` without multiple edges, each edge can be written
93 as a tuple `(u, v)`. Its line graph `L` has the edges of `G` as its
94 nodes. If `x` and `y` are two nodes in `L`, then `(x, y)` is an edge in `L`
95 if and only if the tail of `x` matches the head of `y`, for example, if `x
96 = (a, b)` and `y = (b, c)` for some vertices `a`, `b`, and `c` in `G`.
97
98 Due to the directed nature of the edges, it is no longer the case that
99 every edge in `G` should have a self-loop in `L`. Now, the only time
100 self-loops arise is if a node in `G` itself has a self-loop. So such
101 self-loops are no longer "trivial" but instead, represent essential
102 features of the topology of `G`. For this reason, the historical
103 development of line digraphs is such that self-loops are included. When the
104 graph `G` has multiple edges, once again only superficial changes are
105 required to the definition.
106
107 References
108 ----------
109 * Harary, Frank, and Norman, Robert Z., "Some properties of line digraphs",
110 Rend. Circ. Mat. Palermo, II. Ser. 9 (1960), 161--168.
111 * Hemminger, R. L.; Beineke, L. W. (1978), "Line graphs and line digraphs",
112 in Beineke, L. W.; Wilson, R. J., Selected Topics in Graph Theory,
113 Academic Press Inc., pp. 271--305.
114
115 """
116 if G.is_directed():
117 L = _lg_directed(G, create_using=create_using)
118 else:
119 L = _lg_undirected(G, selfloops=False, create_using=create_using)
120 return L
121
122
123def _lg_directed(G, create_using=None):
124 """Returns the line graph L of the (multi)digraph G.
125
126 Edges in G appear as nodes in L, represented as tuples of the form (u,v)
127 or (u,v,key) if G is a multidigraph. A node in L corresponding to the edge
128 (u,v) is connected to every node corresponding to an edge (v,w).
129
130 Parameters
131 ----------
132 G : digraph
133 A directed graph or directed multigraph.
134 create_using : NetworkX graph constructor, optional
135 Graph type to create. If graph instance, then cleared before populated.
136 Default is to use the same graph class as `G`.
137
138 """
139 L = nx.empty_graph(0, create_using, default=G.__class__)
140
141 # Create a graph specific edge function.
142 get_edges = partial(G.edges, keys=True) if G.is_multigraph() else G.edges
143
144 for from_node in get_edges():
145 # from_node is: (u,v) or (u,v,key)
146 L.add_node(from_node)
147 for to_node in get_edges(from_node[1]):
148 L.add_edge(from_node, to_node)
149
150 return L
151
152
153def _lg_undirected(G, selfloops=False, create_using=None):
154 """Returns the line graph L of the (multi)graph G.
155
156 Edges in G appear as nodes in L, represented as sorted tuples of the form
157 (u,v), or (u,v,key) if G is a multigraph. A node in L corresponding to
158 the edge {u,v} is connected to every node corresponding to an edge that
159 involves u or v.
160
161 Parameters
162 ----------
163 G : graph
164 An undirected graph or multigraph.
165 selfloops : bool
166 If `True`, then self-loops are included in the line graph. If `False`,
167 they are excluded.
168 create_using : NetworkX graph constructor, optional (default=nx.Graph)
169 Graph type to create. If graph instance, then cleared before populated.
170
171 Notes
172 -----
173 The standard algorithm for line graphs of undirected graphs does not
174 produce self-loops.
175
176 """
177 L = nx.empty_graph(0, create_using, default=G.__class__)
178
179 # Graph specific functions for edges.
180 get_edges = partial(G.edges, keys=True) if G.is_multigraph() else G.edges
181
182 # Determine if we include self-loops or not.
183 shift = 0 if selfloops else 1
184
185 # Introduce numbering of nodes
186 node_index = {n: i for i, n in enumerate(G)}
187
188 # Lift canonical representation of nodes to edges in line graph
189 def edge_key_function(edge):
190 return node_index[edge[0]], node_index[edge[1]]
191
192 edges = set()
193 for u in G:
194 # Label nodes as a sorted tuple of nodes in original graph.
195 # Decide on representation of {u, v} as (u, v) or (v, u) depending on node_index.
196 # -> This ensures a canonical representation and avoids comparing values of different types.
197 nodes = [tuple(sorted(x[:2], key=node_index.get)) + x[2:] for x in get_edges(u)]
198
199 if len(nodes) == 1:
200 # Then the edge will be an isolated node in L.
201 L.add_node(nodes[0])
202
203 # Add a clique of `nodes` to graph. To prevent double adding edges,
204 # especially important for multigraphs, we store the edges in
205 # canonical form in a set.
206 for i, a in enumerate(nodes):
207 edges.update(
208 [
209 tuple(sorted((a, b), key=edge_key_function))
210 for b in nodes[i + shift :]
211 ]
212 )
213
214 L.add_edges_from(edges)
215 return L
216
217
218@not_implemented_for("directed")
219@not_implemented_for("multigraph")
220@nx._dispatchable(returns_graph=True)
221def inverse_line_graph(G):
222 """Returns the inverse line graph of graph G.
223
224 If H is a graph, and G is the line graph of H, such that G = L(H).
225 Then H is the inverse line graph of G.
226
227 Not all graphs are line graphs and these do not have an inverse line graph.
228 In these cases this function raises a NetworkXError.
229
230 Parameters
231 ----------
232 G : graph
233 A NetworkX Graph
234
235 Returns
236 -------
237 H : graph
238 The inverse line graph of G.
239
240 Raises
241 ------
242 NetworkXNotImplemented
243 If G is directed or a multigraph
244
245 NetworkXError
246 If G is not a line graph
247
248 Notes
249 -----
250 This is an implementation of the Roussopoulos algorithm[1]_.
251
252 If G consists of multiple components, then the algorithm doesn't work.
253 You should invert every component separately:
254
255 >>> K5 = nx.complete_graph(5)
256 >>> P4 = nx.Graph([("a", "b"), ("b", "c"), ("c", "d")])
257 >>> G = nx.union(K5, P4)
258 >>> root_graphs = []
259 >>> for comp in nx.connected_components(G):
260 ... root_graphs.append(nx.inverse_line_graph(G.subgraph(comp)))
261 >>> len(root_graphs)
262 2
263
264 References
265 ----------
266 .. [1] Roussopoulos, N.D. , "A max {m, n} algorithm for determining the graph H from
267 its line graph G", Information Processing Letters 2, (1973), 108--112, ISSN 0020-0190,
268 `DOI link <https://doi.org/10.1016/0020-0190(73)90029-X>`_
269
270 """
271 if G.number_of_nodes() == 0:
272 return nx.empty_graph(1)
273 elif G.number_of_nodes() == 1:
274 v = arbitrary_element(G)
275 a = (v, 0)
276 b = (v, 1)
277 H = nx.Graph([(a, b)])
278 return H
279 elif G.number_of_nodes() > 1 and G.number_of_edges() == 0:
280 msg = (
281 "inverse_line_graph() doesn't work on an edgeless graph. "
282 "Please use this function on each component separately."
283 )
284 raise nx.NetworkXError(msg)
285
286 if nx.number_of_selfloops(G) != 0:
287 msg = (
288 "A line graph as generated by NetworkX has no selfloops, so G has no "
289 "inverse line graph. Please remove the selfloops from G and try again."
290 )
291 raise nx.NetworkXError(msg)
292
293 starting_cell = _select_starting_cell(G)
294 P = _find_partition(G, starting_cell)
295 # count how many times each vertex appears in the partition set
296 P_count = {u: 0 for u in G.nodes}
297 for p in P:
298 for u in p:
299 P_count[u] += 1
300
301 if max(P_count.values()) > 2:
302 msg = "G is not a line graph (vertex found in more than two partition cells)"
303 raise nx.NetworkXError(msg)
304 W = tuple((u,) for u in P_count if P_count[u] == 1)
305 H = nx.Graph()
306 H.add_nodes_from(P)
307 H.add_nodes_from(W)
308 for a, b in combinations(H.nodes, 2):
309 if any(a_bit in b for a_bit in a):
310 H.add_edge(a, b)
311 return H
312
313
314def _triangles(G, e):
315 """Return list of all triangles containing edge e"""
316 u, v = e
317 if u not in G:
318 raise nx.NetworkXError(f"Vertex {u} not in graph")
319 if v not in G[u]:
320 raise nx.NetworkXError(f"Edge ({u}, {v}) not in graph")
321 triangle_list = []
322 for x in G[u]:
323 if x in G[v]:
324 triangle_list.append((u, v, x))
325 return triangle_list
326
327
328def _odd_triangle(G, T):
329 """Test whether T is an odd triangle in G
330
331 Parameters
332 ----------
333 G : NetworkX Graph
334 T : 3-tuple of vertices forming triangle in G
335
336 Returns
337 -------
338 True is T is an odd triangle
339 False otherwise
340
341 Raises
342 ------
343 NetworkXError
344 T is not a triangle in G
345
346 Notes
347 -----
348 An odd triangle is one in which there exists another vertex in G which is
349 adjacent to either exactly one or exactly all three of the vertices in the
350 triangle.
351
352 """
353 for u in T:
354 if u not in G.nodes():
355 raise nx.NetworkXError(f"Vertex {u} not in graph")
356 for e in list(combinations(T, 2)):
357 if e[0] not in G[e[1]]:
358 raise nx.NetworkXError(f"Edge ({e[0]}, {e[1]}) not in graph")
359
360 T_nbrs = defaultdict(int)
361 for t in T:
362 for v in G[t]:
363 if v not in T:
364 T_nbrs[v] += 1
365 return any(T_nbrs[v] in [1, 3] for v in T_nbrs)
366
367
368def _find_partition(G, starting_cell):
369 """Find a partition of the vertices of G into cells of complete graphs
370
371 Parameters
372 ----------
373 G : NetworkX Graph
374 starting_cell : tuple of vertices in G which form a cell
375
376 Returns
377 -------
378 List of tuples of vertices of G
379
380 Raises
381 ------
382 NetworkXError
383 If a cell is not a complete subgraph then G is not a line graph
384 """
385 G_partition = G.copy()
386 P = [starting_cell] # partition set
387 G_partition.remove_edges_from(list(combinations(starting_cell, 2)))
388 # keep list of partitioned nodes which might have an edge in G_partition
389 partitioned_vertices = list(starting_cell)
390 while G_partition.number_of_edges() > 0:
391 # there are still edges left and so more cells to be made
392 u = partitioned_vertices.pop()
393 deg_u = len(G_partition[u])
394 if deg_u != 0:
395 # if u still has edges then we need to find its other cell
396 # this other cell must be a complete subgraph or else G is
397 # not a line graph
398 new_cell = [u] + list(G_partition[u])
399 for u in new_cell:
400 for v in new_cell:
401 if (u != v) and (v not in G_partition[u]):
402 msg = (
403 "G is not a line graph "
404 "(partition cell not a complete subgraph)"
405 )
406 raise nx.NetworkXError(msg)
407 P.append(tuple(new_cell))
408 G_partition.remove_edges_from(list(combinations(new_cell, 2)))
409 partitioned_vertices += new_cell
410 return P
411
412
413def _select_starting_cell(G, starting_edge=None):
414 """Select a cell to initiate _find_partition
415
416 Parameters
417 ----------
418 G : NetworkX Graph
419 starting_edge: an edge to build the starting cell from
420
421 Returns
422 -------
423 Tuple of vertices in G
424
425 Raises
426 ------
427 NetworkXError
428 If it is determined that G is not a line graph
429
430 Notes
431 -----
432 If starting edge not specified then pick an arbitrary edge - doesn't
433 matter which. However, this function may call itself requiring a
434 specific starting edge. Note that the r, s notation for counting
435 triangles is the same as in the Roussopoulos paper cited above.
436 """
437 if starting_edge is None:
438 e = arbitrary_element(G.edges())
439 else:
440 e = starting_edge
441 if e[0] not in G.nodes():
442 raise nx.NetworkXError(f"Vertex {e[0]} not in graph")
443 if e[1] not in G[e[0]]:
444 msg = f"starting_edge ({e[0]}, {e[1]}) is not in the Graph"
445 raise nx.NetworkXError(msg)
446 e_triangles = _triangles(G, e)
447 r = len(e_triangles)
448 if r == 0:
449 # there are no triangles containing e, so the starting cell is just e
450 starting_cell = e
451 elif r == 1:
452 # there is exactly one triangle, T, containing e. If other 2 edges
453 # of T belong only to this triangle then T is starting cell
454 T = e_triangles[0]
455 a, b, c = T
456 # ab was original edge so check the other 2 edges
457 ac_edges = len(_triangles(G, (a, c)))
458 bc_edges = len(_triangles(G, (b, c)))
459 if ac_edges == 1:
460 if bc_edges == 1:
461 starting_cell = T
462 else:
463 return _select_starting_cell(G, starting_edge=(b, c))
464 else:
465 return _select_starting_cell(G, starting_edge=(a, c))
466 else:
467 # r >= 2 so we need to count the number of odd triangles, s
468 s = 0
469 odd_triangles = []
470 for T in e_triangles:
471 if _odd_triangle(G, T):
472 s += 1
473 odd_triangles.append(T)
474 if r == 2 and s == 0:
475 # in this case either triangle works, so just use T
476 starting_cell = T
477 elif r - 1 <= s <= r:
478 # check if odd triangles containing e form complete subgraph
479 triangle_nodes = set()
480 for T in odd_triangles:
481 for x in T:
482 triangle_nodes.add(x)
483
484 for u in triangle_nodes:
485 for v in triangle_nodes:
486 if u != v and (v not in G[u]):
487 msg = (
488 "G is not a line graph (odd triangles "
489 "do not form complete subgraph)"
490 )
491 raise nx.NetworkXError(msg)
492 # otherwise then we can use this as the starting cell
493 starting_cell = tuple(triangle_nodes)
494
495 else:
496 msg = (
497 "G is not a line graph (incorrect number of "
498 "odd triangles around starting edge)"
499 )
500 raise nx.NetworkXError(msg)
501 return starting_cell