connectivity.py 13 KB

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