Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/networkx/generators/line.py: 12%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

161 statements  

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