Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/networkx/algorithms/minors/contraction.py: 18%

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

95 statements  

1"""Provides functions for computing minors of a graph.""" 

2 

3from itertools import chain, combinations, permutations, product 

4 

5import networkx as nx 

6from networkx import density 

7from networkx.exception import NetworkXException 

8from networkx.utils import arbitrary_element 

9 

10__all__ = [ 

11 "contracted_edge", 

12 "contracted_nodes", 

13 "equivalence_classes", 

14 "identified_nodes", 

15 "quotient_graph", 

16] 

17 

18chaini = chain.from_iterable 

19 

20 

21def equivalence_classes(iterable, relation): 

22 """Returns equivalence classes of `relation` when applied to `iterable`. 

23 

24 The equivalence classes, or blocks, consist of objects from `iterable` 

25 which are all equivalent. They are defined to be equivalent if the 

26 `relation` function returns `True` when passed any two objects from that 

27 class, and `False` otherwise. To define an equivalence relation the 

28 function must be reflexive, symmetric and transitive. 

29 

30 Parameters 

31 ---------- 

32 iterable : list, tuple, or set 

33 An iterable of elements/nodes. 

34 

35 relation : function 

36 A Boolean-valued function that implements an equivalence relation 

37 (reflexive, symmetric, transitive binary relation) on the elements 

38 of `iterable` - it must take two elements and return `True` if 

39 they are related, or `False` if not. 

40 

41 Returns 

42 ------- 

43 set of frozensets 

44 A set of frozensets representing the partition induced by the equivalence 

45 relation function `relation` on the elements of `iterable`. Each 

46 member set in the return set represents an equivalence class, or 

47 block, of the partition. 

48 

49 Duplicate elements will be ignored so it makes the most sense for 

50 `iterable` to be a :class:`set`. 

51 

52 Notes 

53 ----- 

54 This function does not check that `relation` represents an equivalence 

55 relation. You can check that your equivalence classes provide a partition 

56 using `is_partition`. 

57 

58 Examples 

59 -------- 

60 Let `X` be the set of integers from `0` to `9`, and consider an equivalence 

61 relation `R` on `X` of congruence modulo `3`: this means that two integers 

62 `x` and `y` in `X` are equivalent under `R` if they leave the same 

63 remainder when divided by `3`, i.e. `(x - y) mod 3 = 0`. 

64 

65 The equivalence classes of this relation are `{0, 3, 6, 9}`, `{1, 4, 7}`, 

66 `{2, 5, 8}`: `0`, `3`, `6`, `9` are all divisible by `3` and leave zero 

67 remainder; `1`, `4`, `7` leave remainder `1`; while `2`, `5` and `8` leave 

68 remainder `2`. We can see this by calling `equivalence_classes` with 

69 `X` and a function implementation of `R`. 

70 

71 >>> X = set(range(10)) 

72 >>> def mod3(x, y): 

73 ... return (x - y) % 3 == 0 

74 >>> equivalence_classes(X, mod3) # doctest: +SKIP 

75 {frozenset({1, 4, 7}), frozenset({8, 2, 5}), frozenset({0, 9, 3, 6})} 

76 """ 

77 # For simplicity of implementation, we initialize the return value as a 

78 # list of lists, then convert it to a set of sets at the end of the 

79 # function. 

80 blocks = [] 

81 # Determine the equivalence class for each element of the iterable. 

82 for y in iterable: 

83 # Each element y must be in *exactly one* equivalence class. 

84 # 

85 # Each block is guaranteed to be non-empty 

86 for block in blocks: 

87 x = arbitrary_element(block) 

88 if relation(x, y): 

89 block.append(y) 

90 break 

91 else: 

92 # If the element y is not part of any known equivalence class, it 

93 # must be in its own, so we create a new singleton equivalence 

94 # class for it. 

95 blocks.append([y]) 

96 return {frozenset(block) for block in blocks} 

97 

98 

99@nx._dispatchable(edge_attrs="weight", returns_graph=True) 

100def quotient_graph( 

101 G, 

102 partition, 

103 edge_relation=None, 

104 node_data=None, 

105 edge_data=None, 

106 weight="weight", 

107 relabel=False, 

108 create_using=None, 

109): 

110 """Returns the quotient graph of `G` under the specified equivalence 

111 relation on nodes. 

112 

113 Parameters 

114 ---------- 

115 G : NetworkX graph 

116 The graph for which to return the quotient graph with the 

117 specified node relation. 

118 

119 partition : function, or dict or list of lists, tuples or sets 

120 If a function, this function must represent an equivalence 

121 relation on the nodes of `G`. It must take two arguments *u* 

122 and *v* and return True exactly when *u* and *v* are in the 

123 same equivalence class. The equivalence classes form the nodes 

124 in the returned graph. 

125 

126 If a dict of lists/tuples/sets, the keys can be any meaningful 

127 block labels, but the values must be the block lists/tuples/sets 

128 (one list/tuple/set per block), and the blocks must form a valid 

129 partition of the nodes of the graph. That is, each node must be 

130 in exactly one block of the partition. 

131 

132 If a list of sets, the list must form a valid partition of 

133 the nodes of the graph. That is, each node must be in exactly 

134 one block of the partition. 

135 

136 edge_relation : Boolean function with two arguments 

137 This function must represent an edge relation on the *blocks* of 

138 the `partition` of `G`. It must take two arguments, *B* and *C*, 

139 each one a set of nodes, and return True exactly when there should be 

140 an edge joining block *B* to block *C* in the returned graph. 

141 

142 If `edge_relation` is not specified, it is assumed to be the 

143 following relation. Block *B* is related to block *C* if and 

144 only if some node in *B* is adjacent to some node in *C*, 

145 according to the edge set of `G`. 

146 

147 node_data : function 

148 This function takes one argument, *B*, a set of nodes in `G`, 

149 and must return a dictionary representing the node data 

150 attributes to set on the node representing *B* in the quotient graph. 

151 If None, the following node attributes will be set: 

152 

153 * 'graph', the subgraph of the graph `G` that this block 

154 represents, 

155 * 'nnodes', the number of nodes in this block, 

156 * 'nedges', the number of edges within this block, 

157 * 'density', the density of the subgraph of `G` that this 

158 block represents. 

159 

160 edge_data : function 

161 This function takes two arguments, *B* and *C*, each one a set 

162 of nodes, and must return a dictionary representing the edge 

163 data attributes to set on the edge joining *B* and *C*, should 

164 there be an edge joining *B* and *C* in the quotient graph (if 

165 no such edge occurs in the quotient graph as determined by 

166 `edge_relation`, then the output of this function is ignored). 

167 

168 If the quotient graph would be a multigraph, this function is 

169 not applied, since the edge data from each edge in the graph 

170 `G` appears in the edges of the quotient graph. 

171 

172 weight : string or None, optional (default="weight") 

173 The name of an edge attribute that holds the numerical value 

174 used as a weight. If None then each edge has weight 1. 

175 

176 relabel : bool 

177 If True, relabel the nodes of the quotient graph to be 

178 nonnegative integers. Otherwise, the nodes are identified with 

179 :class:`frozenset` instances representing the blocks given in 

180 `partition`. 

181 

182 create_using : NetworkX graph constructor, optional (default=nx.Graph) 

183 Graph type to create. If graph instance, then cleared before populated. 

184 

185 Returns 

186 ------- 

187 NetworkX graph 

188 The quotient graph of `G` under the equivalence relation 

189 specified by `partition`. If the partition were given as a 

190 list of :class:`set` instances and `relabel` is False, 

191 each node will be a :class:`frozenset` corresponding to the same 

192 :class:`set`. 

193 

194 Raises 

195 ------ 

196 NetworkXException 

197 If the given partition is not a valid partition of the nodes of 

198 `G`. 

199 

200 Examples 

201 -------- 

202 The quotient graph of the complete bipartite graph under the "same 

203 neighbors" equivalence relation is `K_2`. Under this relation, two nodes 

204 are equivalent if they are not adjacent but have the same neighbor set. 

205 

206 >>> G = nx.complete_bipartite_graph(2, 3) 

207 >>> same_neighbors = lambda u, v: (u not in G[v] and v not in G[u] and G[u] == G[v]) 

208 >>> Q = nx.quotient_graph(G, same_neighbors) 

209 >>> K2 = nx.complete_graph(2) 

210 >>> nx.is_isomorphic(Q, K2) 

211 True 

212 

213 The quotient graph of a directed graph under the "same strongly connected 

214 component" equivalence relation is the condensation of the graph (see 

215 :func:`condensation`). This example comes from the Wikipedia article 

216 *`Strongly connected component`_*. 

217 

218 >>> G = nx.DiGraph() 

219 >>> edges = [ 

220 ... "ab", 

221 ... "be", 

222 ... "bf", 

223 ... "bc", 

224 ... "cg", 

225 ... "cd", 

226 ... "dc", 

227 ... "dh", 

228 ... "ea", 

229 ... "ef", 

230 ... "fg", 

231 ... "gf", 

232 ... "hd", 

233 ... "hf", 

234 ... ] 

235 >>> G.add_edges_from(tuple(x) for x in edges) 

236 >>> components = list(nx.strongly_connected_components(G)) 

237 >>> sorted(sorted(component) for component in components) 

238 [['a', 'b', 'e'], ['c', 'd', 'h'], ['f', 'g']] 

239 >>> 

240 >>> C = nx.condensation(G, components) 

241 >>> component_of = C.graph["mapping"] 

242 >>> same_component = lambda u, v: component_of[u] == component_of[v] 

243 >>> Q = nx.quotient_graph(G, same_component) 

244 >>> nx.is_isomorphic(C, Q) 

245 True 

246 

247 Node identification can be represented as the quotient of a graph under the 

248 equivalence relation that places the two nodes in one block and each other 

249 node in its own singleton block. 

250 

251 >>> K24 = nx.complete_bipartite_graph(2, 4) 

252 >>> K34 = nx.complete_bipartite_graph(3, 4) 

253 >>> C = nx.contracted_nodes(K34, 1, 2) 

254 >>> nodes = {1, 2} 

255 >>> is_contracted = lambda u, v: u in nodes and v in nodes 

256 >>> Q = nx.quotient_graph(K34, is_contracted) 

257 >>> nx.is_isomorphic(Q, C) 

258 True 

259 >>> nx.is_isomorphic(Q, K24) 

260 True 

261 

262 The blockmodeling technique described in [1]_ can be implemented as a 

263 quotient graph. 

264 

265 >>> G = nx.path_graph(6) 

266 >>> partition = [{0, 1}, {2, 3}, {4, 5}] 

267 >>> M = nx.quotient_graph(G, partition, relabel=True) 

268 >>> list(M.edges()) 

269 [(0, 1), (1, 2)] 

270 

271 Here is the sample example but using partition as a dict of block sets. 

272 

273 >>> G = nx.path_graph(6) 

274 >>> partition = {0: {0, 1}, 2: {2, 3}, 4: {4, 5}} 

275 >>> M = nx.quotient_graph(G, partition, relabel=True) 

276 >>> list(M.edges()) 

277 [(0, 1), (1, 2)] 

278 

279 Partitions can be represented in various ways: 

280 

281 0. a list/tuple/set of block lists/tuples/sets 

282 1. a dict with block labels as keys and blocks lists/tuples/sets as values 

283 2. a dict with block lists/tuples/sets as keys and block labels as values 

284 3. a function from nodes in the original iterable to block labels 

285 4. an equivalence relation function on the target iterable 

286 

287 As `quotient_graph` is designed to accept partitions represented as (0), (1) or 

288 (4) only, the `equivalence_classes` function can be used to get the partitions 

289 in the right form, in order to call `quotient_graph`. 

290 

291 .. _Strongly connected component: https://en.wikipedia.org/wiki/Strongly_connected_component 

292 

293 References 

294 ---------- 

295 .. [1] Patrick Doreian, Vladimir Batagelj, and Anuska Ferligoj. 

296 *Generalized Blockmodeling*. 

297 Cambridge University Press, 2004. 

298 

299 """ 

300 # If the user provided an equivalence relation as a function to compute 

301 # the blocks of the partition on the nodes of G induced by the 

302 # equivalence relation. 

303 if callable(partition): 

304 # equivalence_classes always return partition of whole G. 

305 partition = equivalence_classes(G, partition) 

306 if not nx.community.is_partition(G, partition): 

307 raise nx.NetworkXException( 

308 "Input `partition` is not an equivalence relation for nodes of G" 

309 ) 

310 return _quotient_graph( 

311 G, 

312 partition, 

313 edge_relation, 

314 node_data, 

315 edge_data, 

316 weight, 

317 relabel, 

318 create_using, 

319 ) 

320 

321 # If the partition is a dict, it is assumed to be one where the keys are 

322 # user-defined block labels, and values are block lists, tuples or sets. 

323 if isinstance(partition, dict): 

324 partition = list(partition.values()) 

325 

326 # If the user provided partition as a collection of sets. Then we 

327 # need to check if partition covers all of G nodes. If the answer 

328 # is 'No' then we need to prepare suitable subgraph view. 

329 partition_nodes = set().union(*partition) 

330 if len(partition_nodes) != len(G): 

331 G = G.subgraph(partition_nodes) 

332 # Each node in the graph/subgraph must be in exactly one block. 

333 if not nx.community.is_partition(G, partition): 

334 raise NetworkXException("each node must be in exactly one part of `partition`") 

335 return _quotient_graph( 

336 G, 

337 partition, 

338 edge_relation, 

339 node_data, 

340 edge_data, 

341 weight, 

342 relabel, 

343 create_using, 

344 ) 

345 

346 

347def _quotient_graph( 

348 G, partition, edge_relation, node_data, edge_data, weight, relabel, create_using 

349): 

350 """Construct the quotient graph assuming input has been checked""" 

351 if create_using is None: 

352 H = G.__class__() 

353 else: 

354 H = nx.empty_graph(0, create_using) 

355 # By default set some basic information about the subgraph that each block 

356 # represents on the nodes in the quotient graph. 

357 if node_data is None: 

358 

359 def node_data(b): 

360 S = G.subgraph(b) 

361 return { 

362 "graph": S, 

363 "nnodes": len(S), 

364 "nedges": S.number_of_edges(), 

365 "density": density(S), 

366 } 

367 

368 # Each block of the partition becomes a node in the quotient graph. 

369 partition = [frozenset(b) for b in partition] 

370 H.add_nodes_from((b, node_data(b)) for b in partition) 

371 # By default, the edge relation is the relation defined as follows. B is 

372 # adjacent to C if a node in B is adjacent to a node in C, according to the 

373 # edge set of G. 

374 # 

375 # This is not a particularly efficient implementation of this relation: 

376 # there are O(n^2) pairs to check and each check may require O(log n) time 

377 # (to check set membership). This can certainly be parallelized. 

378 if edge_relation is None: 

379 

380 def edge_relation(b, c): 

381 return any(v in G[u] for u, v in product(b, c)) 

382 

383 # By default, sum the weights of the edges joining pairs of nodes across 

384 # blocks to get the weight of the edge joining those two blocks. 

385 if edge_data is None: 

386 

387 def edge_data(b, c): 

388 edgedata = ( 

389 d 

390 for u, v, d in G.edges(b | c, data=True) 

391 if (u in b and v in c) or (u in c and v in b) 

392 ) 

393 return {"weight": sum(d.get(weight, 1) for d in edgedata)} 

394 

395 block_pairs = permutations(H, 2) if H.is_directed() else combinations(H, 2) 

396 # In a multigraph, add one edge in the quotient graph for each edge 

397 # in the original graph. 

398 if H.is_multigraph(): 

399 edges = chaini( 

400 ( 

401 (b, c, G.get_edge_data(u, v, default={})) 

402 for u, v in product(b, c) 

403 if v in G[u] 

404 ) 

405 for b, c in block_pairs 

406 if edge_relation(b, c) 

407 ) 

408 # In a simple graph, apply the edge data function to each pair of 

409 # blocks to determine the edge data attributes to apply to each edge 

410 # in the quotient graph. 

411 else: 

412 edges = ( 

413 (b, c, edge_data(b, c)) for (b, c) in block_pairs if edge_relation(b, c) 

414 ) 

415 H.add_edges_from(edges) 

416 # If requested by the user, relabel the nodes to be integers, 

417 # numbered in increasing order from zero in the same order as the 

418 # iteration order of `partition`. 

419 if relabel: 

420 # Can't use nx.convert_node_labels_to_integers() here since we 

421 # want the order of iteration to be the same for backward 

422 # compatibility with the nx.blockmodel() function. 

423 labels = {b: i for i, b in enumerate(partition)} 

424 H = nx.relabel_nodes(H, labels) 

425 return H 

426 

427 

428@nx._dispatchable( 

429 preserve_all_attrs=True, mutates_input={"not copy": 4}, returns_graph=True 

430) 

431def contracted_nodes( 

432 G, u, v, self_loops=True, copy=True, *, store_contraction_as="contraction" 

433): 

434 """Returns the graph that results from contracting `u` and `v`. 

435 

436 Node contraction identifies the two nodes as a single node incident to any 

437 edge that was incident to the original two nodes. 

438 Information about the contracted nodes and any modified edges are stored on 

439 the output graph in a ``"contraction"`` attribute - see Examples for details. 

440 

441 Parameters 

442 ---------- 

443 G : NetworkX graph 

444 The graph whose nodes will be contracted. 

445 

446 u, v : nodes 

447 Must be nodes in `G`. 

448 

449 self_loops : Boolean 

450 If this is True, any edges joining `u` and `v` in `G` become 

451 self-loops on the new node in the returned graph. 

452 

453 copy : Boolean 

454 If this is True (the default), make a copy of 

455 `G` and return that instead of directly changing `G`. 

456 

457 store_contraction_as : str or None, default="contraction" 

458 Name of the node/edge attribute where information about the contraction 

459 should be stored. By default information about the contracted node and 

460 any contracted edges is stored in a ``"contraction"`` attribute on the 

461 resulting node and edge. If `None`, information about the contracted 

462 nodes/edges and their data are not stored. 

463 

464 Returns 

465 ------- 

466 Networkx graph 

467 If `copy` is True, 

468 A new graph object of the same type as `G` (leaving `G` unmodified) 

469 with `u` and `v` identified in a single node. The right node `v` 

470 will be merged into the node `u`, so only `u` will appear in the 

471 returned graph. 

472 If `copy` is False, 

473 Modifies `G` with `u` and `v` identified in a single node. 

474 The right node `v` will be merged into the node `u`, so 

475 only `u` will appear in the returned graph. 

476 

477 Notes 

478 ----- 

479 For multigraphs, the edge keys for the realigned edges may 

480 not be the same as the edge keys for the old edges. This is 

481 natural because edge keys are unique only within each pair of nodes. 

482 

483 This function is also available as `identified_nodes`. 

484 

485 Examples 

486 -------- 

487 Contracting two nonadjacent nodes of the cycle graph on four nodes `C_4` 

488 yields the path graph (ignoring parallel edges): 

489 

490 >>> G = nx.cycle_graph(4) 

491 >>> M = nx.contracted_nodes(G, 1, 3) 

492 >>> P3 = nx.path_graph(3) 

493 >>> nx.is_isomorphic(M, P3) 

494 True 

495 

496 Information about the contracted nodes is stored on the resulting graph in 

497 a ``"contraction"`` attribute. For instance, the contracted node is stored 

498 as an attribute on ``u``: 

499 

500 >>> H = nx.contracted_nodes(P3, 0, 2) 

501 >>> H.nodes(data=True) 

502 NodeDataView({0: {'contraction': {2: {}}}, 1: {}}) 

503 

504 Any node attributes on the contracted node are also preserved: 

505 

506 >>> nx.set_node_attributes(P3, dict(enumerate("rgb")), name="color") 

507 >>> P3.nodes(data=True) 

508 NodeDataView({0: {'color': 'r'}, 1: {'color': 'g'}, 2: {'color': 'b'}}) 

509 >>> H = nx.contracted_nodes(P3, 0, 2) 

510 >>> H.nodes[0] 

511 {'color': 'r', 'contraction': {2: {'color': 'b'}}} 

512 

513 Edges are handled similarly: when ``u`` and ``v`` are adjacent to a third node 

514 ``w``, the edge ``(v, w)`` will be contracted into the edge ``(u, w)`` with 

515 its attributes stored into a ``"contraction"`` attribute on edge ``(u, w)``: 

516 

517 >>> nx.set_edge_attributes(P3, {(0, 1): 10, (1, 2): 100}, name="weight") 

518 >>> P3.edges(data=True) 

519 EdgeDataView([(0, 1, {'weight': 10}), (1, 2, {'weight': 100})]) 

520 >>> H = nx.contracted_nodes(P3, 0, 2) 

521 >>> H.edges(data=True) 

522 EdgeDataView([(0, 1, {'weight': 10, 'contraction': {(2, 1): {'weight': 100}}})]) 

523 

524 Attributes from contracted nodes/edges can be combined with those of the 

525 nodes/edges onto which they were contracted: 

526 

527 >>> # Concatenate colors of contracted nodes 

528 >>> for u, cdict in H.nodes(data="contraction"): 

529 ... if cdict is not None: 

530 ... H.nodes[u]["color"] += "".join(n["color"] for n in cdict.values()) 

531 ... del H.nodes[u]["contraction"] # Remove contraction attr (optional) 

532 >>> H.nodes(data=True) 

533 NodeDataView({0: {'color': 'rb'}, 1: {'color': 'g'}}) 

534 >>> # Sum contracted edge weights 

535 >>> for u, v, cdict in H.edges(data="contraction"): 

536 ... if cdict is not None: 

537 ... H[u][v]["weight"] += sum(n["weight"] for n in cdict.values()) 

538 ... del H.edges[(u, v)]["contraction"] # Remove contraction attr (optional) 

539 >>> H.edges(data=True) 

540 EdgeDataView([(0, 1, {'weight': 110})]) 

541 

542 If `G` is a multigraph, then a new edge is added instead. Any edge attributes 

543 are still preserved: 

544 

545 >>> MG = nx.MultiGraph(P3) 

546 >>> MH = nx.contracted_nodes(MG, 0, 2) 

547 >>> MH.edges(keys=True, data=True) 

548 MultiEdgeDataView([(0, 1, 0, {'weight': 10}), (0, 1, 1, {'weight': 100})]) 

549 

550 If ``selfloops=True`` (the default), any edges adjoining `u` and `v` become 

551 self-loops on ``u`` in the resulting graph: 

552 

553 >>> G = nx.Graph([(1, 2)]) 

554 >>> H = nx.contracted_nodes(G, 1, 2) 

555 >>> H.nodes, H.edges 

556 (NodeView((1,)), EdgeView([(1, 1)])) 

557 >>> H = nx.contracted_nodes(G, 1, 2, self_loops=False) 

558 >>> H.nodes, H.edges 

559 (NodeView((1,)), EdgeView([])) 

560 

561 Note however that any self loops in the original graph `G` are preserved: 

562 

563 >>> G = nx.Graph([(1, 2), (2, 2)]) 

564 >>> H = nx.contracted_nodes(G, 1, 2, self_loops=False) 

565 >>> H.nodes, H.edges 

566 (NodeView((1,)), EdgeView([(1, 1)])) 

567 

568 The same reasoning applies to MultiGraphs: 

569 

570 >>> MG = nx.MultiGraph([(1, 2), (2, 2)]) 

571 >>> # Edge (1, 1, 0) in MH corresponds to edge (2, 2) in MG 

572 >>> MH = nx.contracted_nodes(MG, 1, 2, self_loops=False) 

573 >>> MH.edges(keys=True) 

574 MultiEdgeView([(1, 1, 0)]) 

575 >>> # MH has two (1, 1) edges - one from edge (2, 2) in MG, and one 

576 >>> # resulting from the contraction of 2->1 

577 >>> MH = nx.contracted_nodes(MG, 1, 2, self_loops=True) 

578 >>> MH.edges(keys=True) 

579 MultiEdgeView([(1, 1, 0), (1, 1, 1)]) 

580 

581 

582 In a ``MultiDiGraph`` with a self loop, the in and out edges will 

583 be treated separately as edges, so while contracting a node which 

584 has a self loop the contraction will add multiple edges: 

585 

586 >>> G = nx.MultiDiGraph([(1, 2), (2, 2)]) 

587 >>> H = nx.contracted_nodes(G, 1, 2) 

588 >>> list(H.edges()) # edge 1->2, 2->2, 2<-2 from the original Graph G 

589 [(1, 1), (1, 1), (1, 1)] 

590 >>> H = nx.contracted_nodes(G, 1, 2, self_loops=False) 

591 >>> list(H.edges()) # edge 2->2, 2<-2 from the original Graph G 

592 [(1, 1), (1, 1)] 

593 

594 See Also 

595 -------- 

596 contracted_edge 

597 quotient_graph 

598 

599 """ 

600 # Copying has significant overhead and can be disabled if needed 

601 H = G.copy() if copy else G 

602 

603 # edge code uses G.edges(v) instead of G.adj[v] to handle multiedges 

604 if H.is_directed(): 

605 edges_to_remap = chain(G.in_edges(v, data=True), G.out_edges(v, data=True)) 

606 else: 

607 edges_to_remap = G.edges(v, data=True) 

608 

609 # If the H=G, the generators change as H changes 

610 # This makes the edges_to_remap independent of H 

611 if not copy: 

612 edges_to_remap = list(edges_to_remap) 

613 

614 v_data = H.nodes[v] 

615 H.remove_node(v) 

616 

617 # A bit of input munging to extract whether contraction info should be 

618 # stored, and if so bind to a shorter name 

619 if _store_contraction := (store_contraction_as is not None): 

620 contraction = store_contraction_as 

621 

622 for prev_w, prev_x, d in edges_to_remap: 

623 w = prev_w if prev_w != v else u 

624 x = prev_x if prev_x != v else u 

625 

626 if ({prev_w, prev_x} == {u, v}) and not self_loops: 

627 continue 

628 

629 if not H.has_edge(w, x) or G.is_multigraph(): 

630 H.add_edge(w, x, **d) 

631 continue 

632 

633 # Store information about the contracted edge iff `store_contraction` is not None 

634 if _store_contraction: 

635 if contraction in H.edges[(w, x)]: 

636 H.edges[(w, x)][contraction][(prev_w, prev_x)] = d 

637 else: 

638 H.edges[(w, x)][contraction] = {(prev_w, prev_x): d} 

639 

640 # Store information about the contracted node iff `store_contraction` 

641 if _store_contraction: 

642 if contraction in H.nodes[u]: 

643 H.nodes[u][contraction][v] = v_data 

644 else: 

645 H.nodes[u][contraction] = {v: v_data} 

646 

647 return H 

648 

649 

650identified_nodes = contracted_nodes 

651 

652 

653@nx._dispatchable( 

654 preserve_all_attrs=True, mutates_input={"not copy": 3}, returns_graph=True 

655) 

656def contracted_edge( 

657 G, edge, self_loops=True, copy=True, *, store_contraction_as="contraction" 

658): 

659 """Returns the graph that results from contracting the specified edge. 

660 

661 Edge contraction identifies the two endpoints of the edge as a single node 

662 incident to any edge that was incident to the original two nodes. A graph 

663 that results from edge contraction is called a *minor* of the original 

664 graph. 

665 

666 Parameters 

667 ---------- 

668 G : NetworkX graph 

669 The graph whose edge will be contracted. 

670 

671 edge : tuple 

672 Must be a pair of nodes in `G`. 

673 

674 self_loops : Boolean 

675 If this is True, any edges (including `edge`) joining the 

676 endpoints of `edge` in `G` become self-loops on the new node in the 

677 returned graph. 

678 

679 copy : Boolean (default True) 

680 If this is True, a the contraction will be performed on a copy of `G`, 

681 otherwise the contraction will happen in place. 

682 

683 store_contraction_as : str or None, default="contraction" 

684 Name of the node/edge attribute where information about the contraction 

685 should be stored. By default information about the contracted node and 

686 any contracted edges is stored in a ``"contraction"`` attribute on the 

687 resulting node and edge. If `None`, information about the contracted 

688 nodes/edges and their data are not stored. 

689 

690 Returns 

691 ------- 

692 Networkx graph 

693 A new graph object of the same type as `G` (leaving `G` unmodified) 

694 with endpoints of `edge` identified in a single node. The right node 

695 of `edge` will be merged into the left one, so only the left one will 

696 appear in the returned graph. 

697 

698 Raises 

699 ------ 

700 ValueError 

701 If `edge` is not an edge in `G`. 

702 

703 Examples 

704 -------- 

705 Attempting to contract two nonadjacent nodes yields an error: 

706 

707 >>> G = nx.cycle_graph(4) 

708 >>> nx.contracted_edge(G, (1, 3)) 

709 Traceback (most recent call last): 

710 ... 

711 ValueError: Edge (1, 3) does not exist in graph G; cannot contract it 

712 

713 Contracting two adjacent nodes in the cycle graph on *n* nodes yields the 

714 cycle graph on *n - 1* nodes: 

715 

716 >>> C5 = nx.cycle_graph(5) 

717 >>> C4 = nx.cycle_graph(4) 

718 >>> M = nx.contracted_edge(C5, (0, 1), self_loops=False) 

719 >>> nx.is_isomorphic(M, C4) 

720 True 

721 

722 See also 

723 -------- 

724 contracted_nodes 

725 quotient_graph 

726 

727 """ 

728 u, v = edge[:2] 

729 if not G.has_edge(u, v): 

730 raise ValueError(f"Edge {edge} does not exist in graph G; cannot contract it") 

731 return contracted_nodes( 

732 G, 

733 u, 

734 v, 

735 self_loops=self_loops, 

736 copy=copy, 

737 store_contraction_as=store_contraction_as, 

738 )