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

91 statements  

« prev     ^ index     » next       coverage.py v7.3.2, created at 2023-10-20 07:00 +0000

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

2from itertools import chain, combinations, permutations, product 

3 

4import networkx as nx 

5from networkx import density 

6from networkx.exception import NetworkXException 

7from networkx.utils import arbitrary_element 

8 

9__all__ = [ 

10 "contracted_edge", 

11 "contracted_nodes", 

12 "equivalence_classes", 

13 "identified_nodes", 

14 "quotient_graph", 

15] 

16 

17chaini = chain.from_iterable 

18 

19 

20def equivalence_classes(iterable, relation): 

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

22 

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

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

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

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

27 function must be reflexive, symmetric and transitive. 

28 

29 Parameters 

30 ---------- 

31 iterable : list, tuple, or set 

32 An iterable of elements/nodes. 

33 

34 relation : function 

35 A Boolean-valued function that implements an equivalence relation 

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

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

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

39 

40 Returns 

41 ------- 

42 set of frozensets 

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

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

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

46 block, of the partition. 

47 

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

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

50 

51 Notes 

52 ----- 

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

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

55 using `is_partition`. 

56 

57 Examples 

58 -------- 

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

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

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

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

63 

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

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

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

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

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

69 

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

71 >>> def mod3(x, y): return (x - y) % 3 == 0 

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

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

74 """ 

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

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

77 # function. 

78 blocks = [] 

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

80 for y in iterable: 

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

82 # 

83 # Each block is guaranteed to be non-empty 

84 for block in blocks: 

85 x = arbitrary_element(block) 

86 if relation(x, y): 

87 block.append(y) 

88 break 

89 else: 

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

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

92 # class for it. 

93 blocks.append([y]) 

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

95 

96 

97@nx._dispatch(edge_attrs="weight") 

98def quotient_graph( 

99 G, 

100 partition, 

101 edge_relation=None, 

102 node_data=None, 

103 edge_data=None, 

104 weight="weight", 

105 relabel=False, 

106 create_using=None, 

107): 

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

109 relation on nodes. 

110 

111 Parameters 

112 ---------- 

113 G : NetworkX graph 

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

115 specified node relation. 

116 

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

118 If a function, this function must represent an equivalence 

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

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

121 same equivalence class. The equivalence classes form the nodes 

122 in the returned graph. 

123 

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

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

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

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

128 in exactly one block of the partition. 

129 

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

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

132 one block of the partition. 

133 

134 edge_relation : Boolean function with two arguments 

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

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

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

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

139 

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

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

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

143 according to the edge set of `G`. 

144 

145 node_data : function 

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

147 and must return a dictionary representing the node data 

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

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

150 

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

152 represents, 

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

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

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

156 block represents. 

157 

158 edge_data : function 

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

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

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

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

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

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

165 

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

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

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

169 

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

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

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

173 

174 relabel : bool 

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

176 nonnegative integers. Otherwise, the nodes are identified with 

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

178 `partition`. 

179 

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

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

182 

183 Returns 

184 ------- 

185 NetworkX graph 

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

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

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

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

190 :class:`set`. 

191 

192 Raises 

193 ------ 

194 NetworkXException 

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

196 `G`. 

197 

198 Examples 

199 -------- 

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

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

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

203 

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

205 >>> same_neighbors = lambda u, v: ( 

206 ... u not in G[v] and v not in G[u] and G[u] == G[v] 

207 ... ) 

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._dispatch(preserve_all_attrs=True) 

429def contracted_nodes(G, u, v, self_loops=True, copy=True): 

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

431 

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

433 edge that was incident to the original two nodes. 

434 

435 Parameters 

436 ---------- 

437 G : NetworkX graph 

438 The graph whose nodes will be contracted. 

439 

440 u, v : nodes 

441 Must be nodes in `G`. 

442 

443 self_loops : Boolean 

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

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

446 

447 copy : Boolean 

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

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

450 

451 

452 Returns 

453 ------- 

454 Networkx graph 

455 If Copy is True, 

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

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

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

459 returned graph. 

460 If copy is False, 

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

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

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

464 

465 Notes 

466 ----- 

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

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

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

470 

471 For non-multigraphs where `u` and `v` are adjacent to a third node 

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

473 `w`) with its attributes stored into a "contraction" attribute. 

474 

475 This function is also available as `identified_nodes`. 

476 

477 Examples 

478 -------- 

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

480 yields the path graph (ignoring parallel edges): 

481 

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

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

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

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

486 True 

487 

488 >>> G = nx.MultiGraph(P3) 

489 >>> M = nx.contracted_nodes(G, 0, 2) 

490 >>> M.edges 

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

492 

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

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

495 >>> list(H.nodes()) 

496 [1] 

497 >>> list(H.edges()) 

498 [(1, 1)] 

499 

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

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

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

503 

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

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

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

507 [(1, 1), (1, 1), (1, 1)] 

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

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

510 [(1, 1), (1, 1)] 

511 

512 See Also 

513 -------- 

514 contracted_edge 

515 quotient_graph 

516 

517 """ 

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

519 if copy: 

520 H = G.copy() 

521 else: 

522 H = G 

523 

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

525 if H.is_directed(): 

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

527 else: 

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

529 

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

531 # This makes the edges_to_remap independent of H 

532 if not copy: 

533 edges_to_remap = list(edges_to_remap) 

534 

535 v_data = H.nodes[v] 

536 H.remove_node(v) 

537 

538 for prev_w, prev_x, d in edges_to_remap: 

539 w = prev_w if prev_w != v else u 

540 x = prev_x if prev_x != v else u 

541 

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

543 continue 

544 

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

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

547 else: 

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

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

550 else: 

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

552 

553 if "contraction" in H.nodes[u]: 

554 H.nodes[u]["contraction"][v] = v_data 

555 else: 

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

557 return H 

558 

559 

560identified_nodes = contracted_nodes 

561 

562 

563@nx._dispatch(preserve_edge_attrs=True) 

564def contracted_edge(G, edge, self_loops=True, copy=True): 

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

566 

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

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

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

570 graph. 

571 

572 Parameters 

573 ---------- 

574 G : NetworkX graph 

575 The graph whose edge will be contracted. 

576 

577 edge : tuple 

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

579 

580 self_loops : Boolean 

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

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

583 returned graph. 

584 

585 copy : Boolean (default True) 

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

587 otherwise the contraction will happen in place. 

588 

589 Returns 

590 ------- 

591 Networkx graph 

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

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

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

595 appear in the returned graph. 

596 

597 Raises 

598 ------ 

599 ValueError 

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

601 

602 Examples 

603 -------- 

604 Attempting to contract two nonadjacent nodes yields an error: 

605 

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

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

608 Traceback (most recent call last): 

609 ... 

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

611 

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

613 cycle graph on *n - 1* nodes: 

614 

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

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

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

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

619 True 

620 

621 See also 

622 -------- 

623 contracted_nodes 

624 quotient_graph 

625 

626 """ 

627 u, v = edge[:2] 

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

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

630 return contracted_nodes(G, u, v, self_loops=self_loops, copy=copy)