contraction.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602
  1. """Provides functions for computing minors of a graph."""
  2. from itertools import chain, combinations, permutations, product
  3. import networkx as nx
  4. from networkx import density
  5. from networkx.exception import NetworkXException
  6. from networkx.utils import arbitrary_element
  7. __all__ = [
  8. "contracted_edge",
  9. "contracted_nodes",
  10. "equivalence_classes",
  11. "identified_nodes",
  12. "quotient_graph",
  13. ]
  14. chaini = chain.from_iterable
  15. def equivalence_classes(iterable, relation):
  16. """Returns equivalence classes of `relation` when applied to `iterable`.
  17. The equivalence classes, or blocks, consist of objects from `iterable`
  18. which are all equivalent. They are defined to be equivalent if the
  19. `relation` function returns `True` when passed any two objects from that
  20. class, and `False` otherwise. To define an equivalence relation the
  21. function must be reflexive, symmetric and transitive.
  22. Parameters
  23. ----------
  24. iterable : list, tuple, or set
  25. An iterable of elements/nodes.
  26. relation : function
  27. A Boolean-valued function that implements an equivalence relation
  28. (reflexive, symmetric, transitive binary relation) on the elements
  29. of `iterable` - it must take two elements and return `True` if
  30. they are related, or `False` if not.
  31. Returns
  32. -------
  33. set of frozensets
  34. A set of frozensets representing the partition induced by the equivalence
  35. relation function `relation` on the elements of `iterable`. Each
  36. member set in the return set represents an equivalence class, or
  37. block, of the partition.
  38. Duplicate elements will be ignored so it makes the most sense for
  39. `iterable` to be a :class:`set`.
  40. Notes
  41. -----
  42. This function does not check that `relation` represents an equivalence
  43. relation. You can check that your equivalence classes provide a partition
  44. using `is_partition`.
  45. Examples
  46. --------
  47. Let `X` be the set of integers from `0` to `9`, and consider an equivalence
  48. relation `R` on `X` of congruence modulo `3`: this means that two integers
  49. `x` and `y` in `X` are equivalent under `R` if they leave the same
  50. remainder when divided by `3`, i.e. `(x - y) mod 3 = 0`.
  51. The equivalence classes of this relation are `{0, 3, 6, 9}`, `{1, 4, 7}`,
  52. `{2, 5, 8}`: `0`, `3`, `6`, `9` are all divisible by `3` and leave zero
  53. remainder; `1`, `4`, `7` leave remainder `1`; while `2`, `5` and `8` leave
  54. remainder `2`. We can see this by calling `equivalence_classes` with
  55. `X` and a function implementation of `R`.
  56. >>> X = set(range(10))
  57. >>> def mod3(x, y): return (x - y) % 3 == 0
  58. >>> equivalence_classes(X, mod3) # doctest: +SKIP
  59. {frozenset({1, 4, 7}), frozenset({8, 2, 5}), frozenset({0, 9, 3, 6})}
  60. """
  61. # For simplicity of implementation, we initialize the return value as a
  62. # list of lists, then convert it to a set of sets at the end of the
  63. # function.
  64. blocks = []
  65. # Determine the equivalence class for each element of the iterable.
  66. for y in iterable:
  67. # Each element y must be in *exactly one* equivalence class.
  68. #
  69. # Each block is guaranteed to be non-empty
  70. for block in blocks:
  71. x = arbitrary_element(block)
  72. if relation(x, y):
  73. block.append(y)
  74. break
  75. else:
  76. # If the element y is not part of any known equivalence class, it
  77. # must be in its own, so we create a new singleton equivalence
  78. # class for it.
  79. blocks.append([y])
  80. return {frozenset(block) for block in blocks}
  81. def quotient_graph(
  82. G,
  83. partition,
  84. edge_relation=None,
  85. node_data=None,
  86. edge_data=None,
  87. relabel=False,
  88. create_using=None,
  89. ):
  90. """Returns the quotient graph of `G` under the specified equivalence
  91. relation on nodes.
  92. Parameters
  93. ----------
  94. G : NetworkX graph
  95. The graph for which to return the quotient graph with the
  96. specified node relation.
  97. partition : function, or dict or list of lists, tuples or sets
  98. If a function, this function must represent an equivalence
  99. relation on the nodes of `G`. It must take two arguments *u*
  100. and *v* and return True exactly when *u* and *v* are in the
  101. same equivalence class. The equivalence classes form the nodes
  102. in the returned graph.
  103. If a dict of lists/tuples/sets, the keys can be any meaningful
  104. block labels, but the values must be the block lists/tuples/sets
  105. (one list/tuple/set per block), and the blocks must form a valid
  106. partition of the nodes of the graph. That is, each node must be
  107. in exactly one block of the partition.
  108. If a list of sets, the list must form a valid partition of
  109. the nodes of the graph. That is, each node must be in exactly
  110. one block of the partition.
  111. edge_relation : Boolean function with two arguments
  112. This function must represent an edge relation on the *blocks* of
  113. the `partition` of `G`. It must take two arguments, *B* and *C*,
  114. each one a set of nodes, and return True exactly when there should be
  115. an edge joining block *B* to block *C* in the returned graph.
  116. If `edge_relation` is not specified, it is assumed to be the
  117. following relation. Block *B* is related to block *C* if and
  118. only if some node in *B* is adjacent to some node in *C*,
  119. according to the edge set of `G`.
  120. edge_data : function
  121. This function takes two arguments, *B* and *C*, each one a set
  122. of nodes, and must return a dictionary representing the edge
  123. data attributes to set on the edge joining *B* and *C*, should
  124. there be an edge joining *B* and *C* in the quotient graph (if
  125. no such edge occurs in the quotient graph as determined by
  126. `edge_relation`, then the output of this function is ignored).
  127. If the quotient graph would be a multigraph, this function is
  128. not applied, since the edge data from each edge in the graph
  129. `G` appears in the edges of the quotient graph.
  130. node_data : function
  131. This function takes one argument, *B*, a set of nodes in `G`,
  132. and must return a dictionary representing the node data
  133. attributes to set on the node representing *B* in the quotient graph.
  134. If None, the following node attributes will be set:
  135. * 'graph', the subgraph of the graph `G` that this block
  136. represents,
  137. * 'nnodes', the number of nodes in this block,
  138. * 'nedges', the number of edges within this block,
  139. * 'density', the density of the subgraph of `G` that this
  140. block represents.
  141. relabel : bool
  142. If True, relabel the nodes of the quotient graph to be
  143. nonnegative integers. Otherwise, the nodes are identified with
  144. :class:`frozenset` instances representing the blocks given in
  145. `partition`.
  146. create_using : NetworkX graph constructor, optional (default=nx.Graph)
  147. Graph type to create. If graph instance, then cleared before populated.
  148. Returns
  149. -------
  150. NetworkX graph
  151. The quotient graph of `G` under the equivalence relation
  152. specified by `partition`. If the partition were given as a
  153. list of :class:`set` instances and `relabel` is False,
  154. each node will be a :class:`frozenset` corresponding to the same
  155. :class:`set`.
  156. Raises
  157. ------
  158. NetworkXException
  159. If the given partition is not a valid partition of the nodes of
  160. `G`.
  161. Examples
  162. --------
  163. The quotient graph of the complete bipartite graph under the "same
  164. neighbors" equivalence relation is `K_2`. Under this relation, two nodes
  165. are equivalent if they are not adjacent but have the same neighbor set.
  166. >>> G = nx.complete_bipartite_graph(2, 3)
  167. >>> same_neighbors = lambda u, v: (
  168. ... u not in G[v] and v not in G[u] and G[u] == G[v]
  169. ... )
  170. >>> Q = nx.quotient_graph(G, same_neighbors)
  171. >>> K2 = nx.complete_graph(2)
  172. >>> nx.is_isomorphic(Q, K2)
  173. True
  174. The quotient graph of a directed graph under the "same strongly connected
  175. component" equivalence relation is the condensation of the graph (see
  176. :func:`condensation`). This example comes from the Wikipedia article
  177. *`Strongly connected component`_*.
  178. >>> G = nx.DiGraph()
  179. >>> edges = [
  180. ... "ab",
  181. ... "be",
  182. ... "bf",
  183. ... "bc",
  184. ... "cg",
  185. ... "cd",
  186. ... "dc",
  187. ... "dh",
  188. ... "ea",
  189. ... "ef",
  190. ... "fg",
  191. ... "gf",
  192. ... "hd",
  193. ... "hf",
  194. ... ]
  195. >>> G.add_edges_from(tuple(x) for x in edges)
  196. >>> components = list(nx.strongly_connected_components(G))
  197. >>> sorted(sorted(component) for component in components)
  198. [['a', 'b', 'e'], ['c', 'd', 'h'], ['f', 'g']]
  199. >>>
  200. >>> C = nx.condensation(G, components)
  201. >>> component_of = C.graph["mapping"]
  202. >>> same_component = lambda u, v: component_of[u] == component_of[v]
  203. >>> Q = nx.quotient_graph(G, same_component)
  204. >>> nx.is_isomorphic(C, Q)
  205. True
  206. Node identification can be represented as the quotient of a graph under the
  207. equivalence relation that places the two nodes in one block and each other
  208. node in its own singleton block.
  209. >>> K24 = nx.complete_bipartite_graph(2, 4)
  210. >>> K34 = nx.complete_bipartite_graph(3, 4)
  211. >>> C = nx.contracted_nodes(K34, 1, 2)
  212. >>> nodes = {1, 2}
  213. >>> is_contracted = lambda u, v: u in nodes and v in nodes
  214. >>> Q = nx.quotient_graph(K34, is_contracted)
  215. >>> nx.is_isomorphic(Q, C)
  216. True
  217. >>> nx.is_isomorphic(Q, K24)
  218. True
  219. The blockmodeling technique described in [1]_ can be implemented as a
  220. quotient graph.
  221. >>> G = nx.path_graph(6)
  222. >>> partition = [{0, 1}, {2, 3}, {4, 5}]
  223. >>> M = nx.quotient_graph(G, partition, relabel=True)
  224. >>> list(M.edges())
  225. [(0, 1), (1, 2)]
  226. Here is the sample example but using partition as a dict of block sets.
  227. >>> G = nx.path_graph(6)
  228. >>> partition = {0: {0, 1}, 2: {2, 3}, 4: {4, 5}}
  229. >>> M = nx.quotient_graph(G, partition, relabel=True)
  230. >>> list(M.edges())
  231. [(0, 1), (1, 2)]
  232. Partitions can be represented in various ways:
  233. 0. a list/tuple/set of block lists/tuples/sets
  234. 1. a dict with block labels as keys and blocks lists/tuples/sets as values
  235. 2. a dict with block lists/tuples/sets as keys and block labels as values
  236. 3. a function from nodes in the original iterable to block labels
  237. 4. an equivalence relation function on the target iterable
  238. As `quotient_graph` is designed to accept partitions represented as (0), (1) or
  239. (4) only, the `equivalence_classes` function can be used to get the partitions
  240. in the right form, in order to call `quotient_graph`.
  241. .. _Strongly connected component: https://en.wikipedia.org/wiki/Strongly_connected_component
  242. References
  243. ----------
  244. .. [1] Patrick Doreian, Vladimir Batagelj, and Anuska Ferligoj.
  245. *Generalized Blockmodeling*.
  246. Cambridge University Press, 2004.
  247. """
  248. # If the user provided an equivalence relation as a function to compute
  249. # the blocks of the partition on the nodes of G induced by the
  250. # equivalence relation.
  251. if callable(partition):
  252. # equivalence_classes always return partition of whole G.
  253. partition = equivalence_classes(G, partition)
  254. if not nx.community.is_partition(G, partition):
  255. raise nx.NetworkXException(
  256. "Input `partition` is not an equivalence relation for nodes of G"
  257. )
  258. return _quotient_graph(
  259. G, partition, edge_relation, node_data, edge_data, relabel, create_using
  260. )
  261. # If the partition is a dict, it is assumed to be one where the keys are
  262. # user-defined block labels, and values are block lists, tuples or sets.
  263. if isinstance(partition, dict):
  264. partition = list(partition.values())
  265. # If the user provided partition as a collection of sets. Then we
  266. # need to check if partition covers all of G nodes. If the answer
  267. # is 'No' then we need to prepare suitable subgraph view.
  268. partition_nodes = set().union(*partition)
  269. if len(partition_nodes) != len(G):
  270. G = G.subgraph(partition_nodes)
  271. # Each node in the graph/subgraph must be in exactly one block.
  272. if not nx.community.is_partition(G, partition):
  273. raise NetworkXException("each node must be in exactly one part of `partition`")
  274. return _quotient_graph(
  275. G, partition, edge_relation, node_data, edge_data, relabel, create_using
  276. )
  277. def _quotient_graph(
  278. G,
  279. partition,
  280. edge_relation=None,
  281. node_data=None,
  282. edge_data=None,
  283. relabel=False,
  284. create_using=None,
  285. ):
  286. """Construct the quotient graph assuming input has been checked"""
  287. if create_using is None:
  288. H = G.__class__()
  289. else:
  290. H = nx.empty_graph(0, create_using)
  291. # By default set some basic information about the subgraph that each block
  292. # represents on the nodes in the quotient graph.
  293. if node_data is None:
  294. def node_data(b):
  295. S = G.subgraph(b)
  296. return {
  297. "graph": S,
  298. "nnodes": len(S),
  299. "nedges": S.number_of_edges(),
  300. "density": density(S),
  301. }
  302. # Each block of the partition becomes a node in the quotient graph.
  303. partition = [frozenset(b) for b in partition]
  304. H.add_nodes_from((b, node_data(b)) for b in partition)
  305. # By default, the edge relation is the relation defined as follows. B is
  306. # adjacent to C if a node in B is adjacent to a node in C, according to the
  307. # edge set of G.
  308. #
  309. # This is not a particularly efficient implementation of this relation:
  310. # there are O(n^2) pairs to check and each check may require O(log n) time
  311. # (to check set membership). This can certainly be parallelized.
  312. if edge_relation is None:
  313. def edge_relation(b, c):
  314. return any(v in G[u] for u, v in product(b, c))
  315. # By default, sum the weights of the edges joining pairs of nodes across
  316. # blocks to get the weight of the edge joining those two blocks.
  317. if edge_data is None:
  318. def edge_data(b, c):
  319. edgedata = (
  320. d
  321. for u, v, d in G.edges(b | c, data=True)
  322. if (u in b and v in c) or (u in c and v in b)
  323. )
  324. return {"weight": sum(d.get("weight", 1) for d in edgedata)}
  325. block_pairs = permutations(H, 2) if H.is_directed() else combinations(H, 2)
  326. # In a multigraph, add one edge in the quotient graph for each edge
  327. # in the original graph.
  328. if H.is_multigraph():
  329. edges = chaini(
  330. (
  331. (b, c, G.get_edge_data(u, v, default={}))
  332. for u, v in product(b, c)
  333. if v in G[u]
  334. )
  335. for b, c in block_pairs
  336. if edge_relation(b, c)
  337. )
  338. # In a simple graph, apply the edge data function to each pair of
  339. # blocks to determine the edge data attributes to apply to each edge
  340. # in the quotient graph.
  341. else:
  342. edges = (
  343. (b, c, edge_data(b, c)) for (b, c) in block_pairs if edge_relation(b, c)
  344. )
  345. H.add_edges_from(edges)
  346. # If requested by the user, relabel the nodes to be integers,
  347. # numbered in increasing order from zero in the same order as the
  348. # iteration order of `partition`.
  349. if relabel:
  350. # Can't use nx.convert_node_labels_to_integers() here since we
  351. # want the order of iteration to be the same for backward
  352. # compatibility with the nx.blockmodel() function.
  353. labels = {b: i for i, b in enumerate(partition)}
  354. H = nx.relabel_nodes(H, labels)
  355. return H
  356. def contracted_nodes(G, u, v, self_loops=True, copy=True):
  357. """Returns the graph that results from contracting `u` and `v`.
  358. Node contraction identifies the two nodes as a single node incident to any
  359. edge that was incident to the original two nodes.
  360. Parameters
  361. ----------
  362. G : NetworkX graph
  363. The graph whose nodes will be contracted.
  364. u, v : nodes
  365. Must be nodes in `G`.
  366. self_loops : Boolean
  367. If this is True, any edges joining `u` and `v` in `G` become
  368. self-loops on the new node in the returned graph.
  369. copy : Boolean
  370. If this is True (default True), make a copy of
  371. `G` and return that instead of directly changing `G`.
  372. Returns
  373. -------
  374. Networkx graph
  375. If Copy is True,
  376. A new graph object of the same type as `G` (leaving `G` unmodified)
  377. with `u` and `v` identified in a single node. The right node `v`
  378. will be merged into the node `u`, so only `u` will appear in the
  379. returned graph.
  380. If copy is False,
  381. Modifies `G` with `u` and `v` identified in a single node.
  382. The right node `v` will be merged into the node `u`, so
  383. only `u` will appear in the returned graph.
  384. Notes
  385. -----
  386. For multigraphs, the edge keys for the realigned edges may
  387. not be the same as the edge keys for the old edges. This is
  388. natural because edge keys are unique only within each pair of nodes.
  389. For non-multigraphs where `u` and `v` are adjacent to a third node
  390. `w`, the edge (`v`, `w`) will be contracted into the edge (`u`,
  391. `w`) with its attributes stored into a "contraction" attribute.
  392. This function is also available as `identified_nodes`.
  393. Examples
  394. --------
  395. Contracting two nonadjacent nodes of the cycle graph on four nodes `C_4`
  396. yields the path graph (ignoring parallel edges):
  397. >>> G = nx.cycle_graph(4)
  398. >>> M = nx.contracted_nodes(G, 1, 3)
  399. >>> P3 = nx.path_graph(3)
  400. >>> nx.is_isomorphic(M, P3)
  401. True
  402. >>> G = nx.MultiGraph(P3)
  403. >>> M = nx.contracted_nodes(G, 0, 2)
  404. >>> M.edges
  405. MultiEdgeView([(0, 1, 0), (0, 1, 1)])
  406. >>> G = nx.Graph([(1, 2), (2, 2)])
  407. >>> H = nx.contracted_nodes(G, 1, 2, self_loops=False)
  408. >>> list(H.nodes())
  409. [1]
  410. >>> list(H.edges())
  411. [(1, 1)]
  412. See Also
  413. --------
  414. contracted_edge
  415. quotient_graph
  416. """
  417. # Copying has significant overhead and can be disabled if needed
  418. if copy:
  419. H = G.copy()
  420. else:
  421. H = G
  422. # edge code uses G.edges(v) instead of G.adj[v] to handle multiedges
  423. if H.is_directed():
  424. edges_to_remap = chain(G.in_edges(v, data=True), G.out_edges(v, data=True))
  425. else:
  426. edges_to_remap = G.edges(v, data=True)
  427. # If the H=G, the generators change as H changes
  428. # This makes the edges_to_remap independent of H
  429. if not copy:
  430. edges_to_remap = list(edges_to_remap)
  431. v_data = H.nodes[v]
  432. H.remove_node(v)
  433. for prev_w, prev_x, d in edges_to_remap:
  434. w = prev_w if prev_w != v else u
  435. x = prev_x if prev_x != v else u
  436. if ({prev_w, prev_x} == {u, v}) and not self_loops:
  437. continue
  438. if not H.has_edge(w, x) or G.is_multigraph():
  439. H.add_edge(w, x, **d)
  440. else:
  441. if "contraction" in H.edges[(w, x)]:
  442. H.edges[(w, x)]["contraction"][(prev_w, prev_x)] = d
  443. else:
  444. H.edges[(w, x)]["contraction"] = {(prev_w, prev_x): d}
  445. if "contraction" in H.nodes[u]:
  446. H.nodes[u]["contraction"][v] = v_data
  447. else:
  448. H.nodes[u]["contraction"] = {v: v_data}
  449. return H
  450. identified_nodes = contracted_nodes
  451. def contracted_edge(G, edge, self_loops=True, copy=True):
  452. """Returns the graph that results from contracting the specified edge.
  453. Edge contraction identifies the two endpoints of the edge as a single node
  454. incident to any edge that was incident to the original two nodes. A graph
  455. that results from edge contraction is called a *minor* of the original
  456. graph.
  457. Parameters
  458. ----------
  459. G : NetworkX graph
  460. The graph whose edge will be contracted.
  461. edge : tuple
  462. Must be a pair of nodes in `G`.
  463. self_loops : Boolean
  464. If this is True, any edges (including `edge`) joining the
  465. endpoints of `edge` in `G` become self-loops on the new node in the
  466. returned graph.
  467. copy : Boolean (default True)
  468. If this is True, a the contraction will be performed on a copy of `G`,
  469. otherwise the contraction will happen in place.
  470. Returns
  471. -------
  472. Networkx graph
  473. A new graph object of the same type as `G` (leaving `G` unmodified)
  474. with endpoints of `edge` identified in a single node. The right node
  475. of `edge` will be merged into the left one, so only the left one will
  476. appear in the returned graph.
  477. Raises
  478. ------
  479. ValueError
  480. If `edge` is not an edge in `G`.
  481. Examples
  482. --------
  483. Attempting to contract two nonadjacent nodes yields an error:
  484. >>> G = nx.cycle_graph(4)
  485. >>> nx.contracted_edge(G, (1, 3))
  486. Traceback (most recent call last):
  487. ...
  488. ValueError: Edge (1, 3) does not exist in graph G; cannot contract it
  489. Contracting two adjacent nodes in the cycle graph on *n* nodes yields the
  490. cycle graph on *n - 1* nodes:
  491. >>> C5 = nx.cycle_graph(5)
  492. >>> C4 = nx.cycle_graph(4)
  493. >>> M = nx.contracted_edge(C5, (0, 1), self_loops=False)
  494. >>> nx.is_isomorphic(M, C4)
  495. True
  496. See also
  497. --------
  498. contracted_nodes
  499. quotient_graph
  500. """
  501. u, v = edge[:2]
  502. if not G.has_edge(u, v):
  503. raise ValueError(f"Edge {edge} does not exist in graph G; cannot contract it")
  504. return contracted_nodes(G, u, v, self_loops=self_loops, copy=copy)