connectivity.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412
  1. """Fast approximation for node connectivity"""
  2. import itertools
  3. from operator import itemgetter
  4. import networkx as nx
  5. __all__ = [
  6. "local_node_connectivity",
  7. "node_connectivity",
  8. "all_pairs_node_connectivity",
  9. ]
  10. @nx._dispatchable(name="approximate_local_node_connectivity")
  11. def local_node_connectivity(G, source, target, cutoff=None):
  12. """Compute node connectivity between source and target.
  13. Pairwise or local node connectivity between two distinct and nonadjacent
  14. nodes is the minimum number of nodes that must be removed (minimum
  15. separating cutset) to disconnect them. By Menger's theorem, this is equal
  16. to the number of node independent paths (paths that share no nodes other
  17. than source and target). Which is what we compute in this function.
  18. This algorithm is a fast approximation that gives an strict lower
  19. bound on the actual number of node independent paths between two nodes [1]_.
  20. It works for both directed and undirected graphs.
  21. Parameters
  22. ----------
  23. G : NetworkX graph
  24. source : node
  25. Starting node for node connectivity
  26. target : node
  27. Ending node for node connectivity
  28. cutoff : integer
  29. Maximum node connectivity to consider. If None, the minimum degree
  30. of source or target is used as a cutoff. Default value None.
  31. Returns
  32. -------
  33. k: integer
  34. pairwise node connectivity
  35. Examples
  36. --------
  37. >>> # Platonic octahedral graph has node connectivity 4
  38. >>> # for each non adjacent node pair
  39. >>> from networkx.algorithms import approximation as approx
  40. >>> G = nx.octahedral_graph()
  41. >>> approx.local_node_connectivity(G, 0, 5)
  42. 4
  43. Notes
  44. -----
  45. This algorithm [1]_ finds node independents paths between two nodes by
  46. computing their shortest path using BFS, marking the nodes of the path
  47. found as 'used' and then searching other shortest paths excluding the
  48. nodes marked as used until no more paths exist. It is not exact because
  49. a shortest path could use nodes that, if the path were longer, may belong
  50. to two different node independent paths. Thus it only guarantees an
  51. strict lower bound on node connectivity.
  52. Note that the authors propose a further refinement, losing accuracy and
  53. gaining speed, which is not implemented yet.
  54. See also
  55. --------
  56. all_pairs_node_connectivity
  57. node_connectivity
  58. References
  59. ----------
  60. .. [1] White, Douglas R., and Mark Newman. 2001 A Fast Algorithm for
  61. Node-Independent Paths. Santa Fe Institute Working Paper #01-07-035
  62. http://eclectic.ss.uci.edu/~drwhite/working.pdf
  63. """
  64. if target == source:
  65. raise nx.NetworkXError("source and target have to be different nodes.")
  66. # Maximum possible node independent paths
  67. if G.is_directed():
  68. possible = min(G.out_degree(source), G.in_degree(target))
  69. else:
  70. possible = min(G.degree(source), G.degree(target))
  71. K = 0
  72. if not possible:
  73. return K
  74. if cutoff is None:
  75. cutoff = float("inf")
  76. exclude = set()
  77. for i in range(min(possible, cutoff)):
  78. try:
  79. path = _bidirectional_shortest_path(G, source, target, exclude)
  80. exclude.update(set(path))
  81. K += 1
  82. except nx.NetworkXNoPath:
  83. break
  84. return K
  85. @nx._dispatchable(name="approximate_node_connectivity")
  86. def node_connectivity(G, s=None, t=None):
  87. r"""Returns an approximation for node connectivity for a graph or digraph G.
  88. Node connectivity is equal to the minimum number of nodes that
  89. must be removed to disconnect G or render it trivial. By Menger's theorem,
  90. this is equal to the number of node independent paths (paths that
  91. share no nodes other than source and target).
  92. If source and target nodes are provided, this function returns the
  93. local node connectivity: the minimum number of nodes that must be
  94. removed to break all paths from source to target in G.
  95. This algorithm is based on a fast approximation that gives an strict lower
  96. bound on the actual number of node independent paths between two nodes [1]_.
  97. It works for both directed and undirected graphs.
  98. Parameters
  99. ----------
  100. G : NetworkX graph
  101. Undirected graph
  102. s : node
  103. Source node. Optional. Default value: None.
  104. t : node
  105. Target node. Optional. Default value: None.
  106. Returns
  107. -------
  108. K : integer
  109. Node connectivity of G, or local node connectivity if source
  110. and target are provided.
  111. Examples
  112. --------
  113. >>> # Platonic octahedral graph is 4-node-connected
  114. >>> from networkx.algorithms import approximation as approx
  115. >>> G = nx.octahedral_graph()
  116. >>> approx.node_connectivity(G)
  117. 4
  118. Notes
  119. -----
  120. This algorithm [1]_ finds node independents paths between two nodes by
  121. computing their shortest path using BFS, marking the nodes of the path
  122. found as 'used' and then searching other shortest paths excluding the
  123. nodes marked as used until no more paths exist. It is not exact because
  124. a shortest path could use nodes that, if the path were longer, may belong
  125. to two different node independent paths. Thus it only guarantees an
  126. strict lower bound on node connectivity.
  127. See also
  128. --------
  129. all_pairs_node_connectivity
  130. local_node_connectivity
  131. References
  132. ----------
  133. .. [1] White, Douglas R., and Mark Newman. 2001 A Fast Algorithm for
  134. Node-Independent Paths. Santa Fe Institute Working Paper #01-07-035
  135. http://eclectic.ss.uci.edu/~drwhite/working.pdf
  136. """
  137. if (s is not None and t is None) or (s is None and t is not None):
  138. raise nx.NetworkXError("Both source and target must be specified.")
  139. # Local node connectivity
  140. if s is not None and t is not None:
  141. if s not in G:
  142. raise nx.NetworkXError(f"node {s} not in graph")
  143. if t not in G:
  144. raise nx.NetworkXError(f"node {t} not in graph")
  145. return local_node_connectivity(G, s, t)
  146. # Global node connectivity
  147. if G.is_directed():
  148. connected_func = nx.is_weakly_connected
  149. iter_func = itertools.permutations
  150. def neighbors(v):
  151. return itertools.chain(G.predecessors(v), G.successors(v))
  152. else:
  153. connected_func = nx.is_connected
  154. iter_func = itertools.combinations
  155. neighbors = G.neighbors
  156. if not connected_func(G):
  157. return 0
  158. # Choose a node with minimum degree
  159. v, minimum_degree = min(G.degree(), key=itemgetter(1))
  160. # Node connectivity is bounded by minimum degree
  161. K = minimum_degree
  162. # compute local node connectivity with all non-neighbors nodes
  163. # and store the minimum
  164. for w in set(G) - set(neighbors(v)) - {v}:
  165. K = min(K, local_node_connectivity(G, v, w, cutoff=K))
  166. # Same for non adjacent pairs of neighbors of v
  167. for x, y in iter_func(neighbors(v), 2):
  168. if y not in G[x] and x != y:
  169. K = min(K, local_node_connectivity(G, x, y, cutoff=K))
  170. return K
  171. @nx._dispatchable(name="approximate_all_pairs_node_connectivity")
  172. def all_pairs_node_connectivity(G, nbunch=None, cutoff=None):
  173. """Compute node connectivity between all pairs of nodes.
  174. Pairwise or local node connectivity between two distinct and nonadjacent
  175. nodes is the minimum number of nodes that must be removed (minimum
  176. separating cutset) to disconnect them. By Menger's theorem, this is equal
  177. to the number of node independent paths (paths that share no nodes other
  178. than source and target). Which is what we compute in this function.
  179. This algorithm is a fast approximation that gives an strict lower
  180. bound on the actual number of node independent paths between two nodes [1]_.
  181. It works for both directed and undirected graphs.
  182. Parameters
  183. ----------
  184. G : NetworkX graph
  185. nbunch: container
  186. Container of nodes. If provided node connectivity will be computed
  187. only over pairs of nodes in nbunch.
  188. cutoff : integer
  189. Maximum node connectivity to consider. If None, the minimum degree
  190. of source or target is used as a cutoff in each pair of nodes.
  191. Default value None.
  192. Returns
  193. -------
  194. K : dictionary
  195. Dictionary, keyed by source and target, of pairwise node connectivity
  196. Examples
  197. --------
  198. A 3 node cycle with one extra node attached has connectivity 2 between all
  199. nodes in the cycle and connectivity 1 between the extra node and the rest:
  200. >>> G = nx.cycle_graph(3)
  201. >>> G.add_edge(2, 3)
  202. >>> import pprint # for nice dictionary formatting
  203. >>> pprint.pprint(nx.all_pairs_node_connectivity(G))
  204. {0: {1: 2, 2: 2, 3: 1},
  205. 1: {0: 2, 2: 2, 3: 1},
  206. 2: {0: 2, 1: 2, 3: 1},
  207. 3: {0: 1, 1: 1, 2: 1}}
  208. See Also
  209. --------
  210. local_node_connectivity
  211. node_connectivity
  212. References
  213. ----------
  214. .. [1] White, Douglas R., and Mark Newman. 2001 A Fast Algorithm for
  215. Node-Independent Paths. Santa Fe Institute Working Paper #01-07-035
  216. http://eclectic.ss.uci.edu/~drwhite/working.pdf
  217. """
  218. if nbunch is None:
  219. nbunch = G
  220. else:
  221. nbunch = set(nbunch)
  222. directed = G.is_directed()
  223. if directed:
  224. iter_func = itertools.permutations
  225. else:
  226. iter_func = itertools.combinations
  227. all_pairs = {n: {} for n in nbunch}
  228. for u, v in iter_func(nbunch, 2):
  229. k = local_node_connectivity(G, u, v, cutoff=cutoff)
  230. all_pairs[u][v] = k
  231. if not directed:
  232. all_pairs[v][u] = k
  233. return all_pairs
  234. def _bidirectional_shortest_path(G, source, target, exclude):
  235. """Returns shortest path between source and target ignoring nodes in the
  236. container 'exclude'.
  237. Parameters
  238. ----------
  239. G : NetworkX graph
  240. source : node
  241. Starting node for path
  242. target : node
  243. Ending node for path
  244. exclude: container
  245. Container for nodes to exclude from the search for shortest paths
  246. Returns
  247. -------
  248. path: list
  249. Shortest path between source and target ignoring nodes in 'exclude'
  250. Raises
  251. ------
  252. NetworkXNoPath
  253. If there is no path or if nodes are adjacent and have only one path
  254. between them
  255. Notes
  256. -----
  257. This function and its helper are originally from
  258. networkx.algorithms.shortest_paths.unweighted and are modified to
  259. accept the extra parameter 'exclude', which is a container for nodes
  260. already used in other paths that should be ignored.
  261. References
  262. ----------
  263. .. [1] White, Douglas R., and Mark Newman. 2001 A Fast Algorithm for
  264. Node-Independent Paths. Santa Fe Institute Working Paper #01-07-035
  265. http://eclectic.ss.uci.edu/~drwhite/working.pdf
  266. """
  267. # call helper to do the real work
  268. results = _bidirectional_pred_succ(G, source, target, exclude)
  269. pred, succ, w = results
  270. # build path from pred+w+succ
  271. path = []
  272. # from source to w
  273. while w is not None:
  274. path.append(w)
  275. w = pred[w]
  276. path.reverse()
  277. # from w to target
  278. w = succ[path[-1]]
  279. while w is not None:
  280. path.append(w)
  281. w = succ[w]
  282. return path
  283. def _bidirectional_pred_succ(G, source, target, exclude):
  284. # does BFS from both source and target and meets in the middle
  285. # excludes nodes in the container "exclude" from the search
  286. # handle either directed or undirected
  287. if G.is_directed():
  288. Gpred = G.predecessors
  289. Gsucc = G.successors
  290. else:
  291. Gpred = G.neighbors
  292. Gsucc = G.neighbors
  293. # predecessor and successors in search
  294. pred = {source: None}
  295. succ = {target: None}
  296. # initialize fringes, start with forward
  297. forward_fringe = [source]
  298. reverse_fringe = [target]
  299. level = 0
  300. while forward_fringe and reverse_fringe:
  301. # Make sure that we iterate one step forward and one step backwards
  302. # thus source and target will only trigger "found path" when they are
  303. # adjacent and then they can be safely included in the container 'exclude'
  304. level += 1
  305. if level % 2 != 0:
  306. this_level = forward_fringe
  307. forward_fringe = []
  308. for v in this_level:
  309. for w in Gsucc(v):
  310. if w in exclude:
  311. continue
  312. if w not in pred:
  313. forward_fringe.append(w)
  314. pred[w] = v
  315. if w in succ:
  316. return pred, succ, w # found path
  317. else:
  318. this_level = reverse_fringe
  319. reverse_fringe = []
  320. for v in this_level:
  321. for w in Gpred(v):
  322. if w in exclude:
  323. continue
  324. if w not in succ:
  325. succ[w] = v
  326. reverse_fringe.append(w)
  327. if w in pred:
  328. return pred, succ, w # found path
  329. raise nx.NetworkXNoPath(f"No path between {source} and {target}.")