distance_measures.py 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765
  1. """Graph diameter, radius, eccentricity and other properties."""
  2. import networkx as nx
  3. from networkx.utils import not_implemented_for
  4. __all__ = [
  5. "eccentricity",
  6. "diameter",
  7. "radius",
  8. "periphery",
  9. "center",
  10. "barycenter",
  11. "resistance_distance",
  12. ]
  13. def _extrema_bounding(G, compute="diameter", weight=None):
  14. """Compute requested extreme distance metric of undirected graph G
  15. Computation is based on smart lower and upper bounds, and in practice
  16. linear in the number of nodes, rather than quadratic (except for some
  17. border cases such as complete graphs or circle shaped graphs).
  18. Parameters
  19. ----------
  20. G : NetworkX graph
  21. An undirected graph
  22. compute : string denoting the requesting metric
  23. "diameter" for the maximal eccentricity value,
  24. "radius" for the minimal eccentricity value,
  25. "periphery" for the set of nodes with eccentricity equal to the diameter,
  26. "center" for the set of nodes with eccentricity equal to the radius,
  27. "eccentricities" for the maximum distance from each node to all other nodes in G
  28. weight : string, function, or None
  29. If this is a string, then edge weights will be accessed via the
  30. edge attribute with this key (that is, the weight of the edge
  31. joining `u` to `v` will be ``G.edges[u, v][weight]``). If no
  32. such edge attribute exists, the weight of the edge is assumed to
  33. be one.
  34. If this is a function, the weight of an edge is the value
  35. returned by the function. The function must accept exactly three
  36. positional arguments: the two endpoints of an edge and the
  37. dictionary of edge attributes for that edge. The function must
  38. return a number.
  39. If this is None, every edge has weight/distance/cost 1.
  40. Weights stored as floating point values can lead to small round-off
  41. errors in distances. Use integer weights to avoid this.
  42. Weights should be positive, since they are distances.
  43. Returns
  44. -------
  45. value : value of the requested metric
  46. int for "diameter" and "radius" or
  47. list of nodes for "center" and "periphery" or
  48. dictionary of eccentricity values keyed by node for "eccentricities"
  49. Raises
  50. ------
  51. NetworkXError
  52. If the graph consists of multiple components
  53. ValueError
  54. If `compute` is not one of "diameter", "radius", "periphery", "center", or "eccentricities".
  55. Notes
  56. -----
  57. This algorithm was proposed in [1]_ and discussed further in [2]_ and [3]_.
  58. References
  59. ----------
  60. .. [1] F. W. Takes, W. A. Kosters,
  61. "Determining the diameter of small world networks."
  62. Proceedings of the 20th ACM international conference on Information and knowledge management, 2011
  63. https://dl.acm.org/doi/abs/10.1145/2063576.2063748
  64. .. [2] F. W. Takes, W. A. Kosters,
  65. "Computing the Eccentricity Distribution of Large Graphs."
  66. Algorithms, 2013
  67. https://www.mdpi.com/1999-4893/6/1/100
  68. .. [3] M. Borassi, P. Crescenzi, M. Habib, W. A. Kosters, A. Marino, F. W. Takes,
  69. "Fast diameter and radius BFS-based computation in (weakly connected) real-world graphs: With an application to the six degrees of separation games. "
  70. Theoretical Computer Science, 2015
  71. https://www.sciencedirect.com/science/article/pii/S0304397515001644
  72. """
  73. # init variables
  74. degrees = dict(G.degree()) # start with the highest degree node
  75. minlowernode = max(degrees, key=degrees.get)
  76. N = len(degrees) # number of nodes
  77. # alternate between smallest lower and largest upper bound
  78. high = False
  79. # status variables
  80. ecc_lower = dict.fromkeys(G, 0)
  81. ecc_upper = dict.fromkeys(G, N)
  82. candidates = set(G)
  83. # (re)set bound extremes
  84. minlower = N
  85. maxlower = 0
  86. minupper = N
  87. maxupper = 0
  88. # repeat the following until there are no more candidates
  89. while candidates:
  90. if high:
  91. current = maxuppernode # select node with largest upper bound
  92. else:
  93. current = minlowernode # select node with smallest lower bound
  94. high = not high
  95. # get distances from/to current node and derive eccentricity
  96. dist = nx.shortest_path_length(G, source=current, weight=weight)
  97. if len(dist) != N:
  98. msg = "Cannot compute metric because graph is not connected."
  99. raise nx.NetworkXError(msg)
  100. current_ecc = max(dist.values())
  101. # print status update
  102. # print ("ecc of " + str(current) + " (" + str(ecc_lower[current]) + "/"
  103. # + str(ecc_upper[current]) + ", deg: " + str(dist[current]) + ") is "
  104. # + str(current_ecc))
  105. # print(ecc_upper)
  106. # (re)set bound extremes
  107. maxuppernode = None
  108. minlowernode = None
  109. # update node bounds
  110. for i in candidates:
  111. # update eccentricity bounds
  112. d = dist[i]
  113. ecc_lower[i] = low = max(ecc_lower[i], max(d, (current_ecc - d)))
  114. ecc_upper[i] = upp = min(ecc_upper[i], current_ecc + d)
  115. # update min/max values of lower and upper bounds
  116. minlower = min(ecc_lower[i], minlower)
  117. maxlower = max(ecc_lower[i], maxlower)
  118. minupper = min(ecc_upper[i], minupper)
  119. maxupper = max(ecc_upper[i], maxupper)
  120. # update candidate set
  121. if compute == "diameter":
  122. ruled_out = {
  123. i
  124. for i in candidates
  125. if ecc_upper[i] <= maxlower and 2 * ecc_lower[i] >= maxupper
  126. }
  127. elif compute == "radius":
  128. ruled_out = {
  129. i
  130. for i in candidates
  131. if ecc_lower[i] >= minupper and ecc_upper[i] + 1 <= 2 * minlower
  132. }
  133. elif compute == "periphery":
  134. ruled_out = {
  135. i
  136. for i in candidates
  137. if ecc_upper[i] < maxlower
  138. and (maxlower == maxupper or ecc_lower[i] > maxupper)
  139. }
  140. elif compute == "center":
  141. ruled_out = {
  142. i
  143. for i in candidates
  144. if ecc_lower[i] > minupper
  145. and (minlower == minupper or ecc_upper[i] + 1 < 2 * minlower)
  146. }
  147. elif compute == "eccentricities":
  148. ruled_out = set()
  149. else:
  150. msg = "compute must be one of 'diameter', 'radius', 'periphery', 'center', 'eccentricities'"
  151. raise ValueError(msg)
  152. ruled_out.update(i for i in candidates if ecc_lower[i] == ecc_upper[i])
  153. candidates -= ruled_out
  154. # for i in ruled_out:
  155. # print("removing %g: ecc_u: %g maxl: %g ecc_l: %g maxu: %g"%
  156. # (i,ecc_upper[i],maxlower,ecc_lower[i],maxupper))
  157. # print("node %g: ecc_u: %g maxl: %g ecc_l: %g maxu: %g"%
  158. # (4,ecc_upper[4],maxlower,ecc_lower[4],maxupper))
  159. # print("NODE 4: %g"%(ecc_upper[4] <= maxlower))
  160. # print("NODE 4: %g"%(2 * ecc_lower[4] >= maxupper))
  161. # print("NODE 4: %g"%(ecc_upper[4] <= maxlower
  162. # and 2 * ecc_lower[4] >= maxupper))
  163. # updating maxuppernode and minlowernode for selection in next round
  164. for i in candidates:
  165. if (
  166. minlowernode is None
  167. or (
  168. ecc_lower[i] == ecc_lower[minlowernode]
  169. and degrees[i] > degrees[minlowernode]
  170. )
  171. or (ecc_lower[i] < ecc_lower[minlowernode])
  172. ):
  173. minlowernode = i
  174. if (
  175. maxuppernode is None
  176. or (
  177. ecc_upper[i] == ecc_upper[maxuppernode]
  178. and degrees[i] > degrees[maxuppernode]
  179. )
  180. or (ecc_upper[i] > ecc_upper[maxuppernode])
  181. ):
  182. maxuppernode = i
  183. # print status update
  184. # print (" min=" + str(minlower) + "/" + str(minupper) +
  185. # " max=" + str(maxlower) + "/" + str(maxupper) +
  186. # " candidates: " + str(len(candidates)))
  187. # print("cand:",candidates)
  188. # print("ecc_l",ecc_lower)
  189. # print("ecc_u",ecc_upper)
  190. # wait = input("press Enter to continue")
  191. # return the correct value of the requested metric
  192. if compute == "diameter":
  193. return maxlower
  194. if compute == "radius":
  195. return minupper
  196. if compute == "periphery":
  197. p = [v for v in G if ecc_lower[v] == maxlower]
  198. return p
  199. if compute == "center":
  200. c = [v for v in G if ecc_upper[v] == minupper]
  201. return c
  202. if compute == "eccentricities":
  203. return ecc_lower
  204. return None
  205. def eccentricity(G, v=None, sp=None, weight=None):
  206. """Returns the eccentricity of nodes in G.
  207. The eccentricity of a node v is the maximum distance from v to
  208. all other nodes in G.
  209. Parameters
  210. ----------
  211. G : NetworkX graph
  212. A graph
  213. v : node, optional
  214. Return value of specified node
  215. sp : dict of dicts, optional
  216. All pairs shortest path lengths as a dictionary of dictionaries
  217. weight : string, function, or None (default=None)
  218. If this is a string, then edge weights will be accessed via the
  219. edge attribute with this key (that is, the weight of the edge
  220. joining `u` to `v` will be ``G.edges[u, v][weight]``). If no
  221. such edge attribute exists, the weight of the edge is assumed to
  222. be one.
  223. If this is a function, the weight of an edge is the value
  224. returned by the function. The function must accept exactly three
  225. positional arguments: the two endpoints of an edge and the
  226. dictionary of edge attributes for that edge. The function must
  227. return a number.
  228. If this is None, every edge has weight/distance/cost 1.
  229. Weights stored as floating point values can lead to small round-off
  230. errors in distances. Use integer weights to avoid this.
  231. Weights should be positive, since they are distances.
  232. Returns
  233. -------
  234. ecc : dictionary
  235. A dictionary of eccentricity values keyed by node.
  236. Examples
  237. --------
  238. >>> G = nx.Graph([(1, 2), (1, 3), (1, 4), (3, 4), (3, 5), (4, 5)])
  239. >>> dict(nx.eccentricity(G))
  240. {1: 2, 2: 3, 3: 2, 4: 2, 5: 3}
  241. >>> dict(nx.eccentricity(G, v=[1, 5])) # This returns the eccentrity of node 1 & 5
  242. {1: 2, 5: 3}
  243. """
  244. # if v is None: # none, use entire graph
  245. # nodes=G.nodes()
  246. # elif v in G: # is v a single node
  247. # nodes=[v]
  248. # else: # assume v is a container of nodes
  249. # nodes=v
  250. order = G.order()
  251. e = {}
  252. for n in G.nbunch_iter(v):
  253. if sp is None:
  254. length = nx.shortest_path_length(G, source=n, weight=weight)
  255. L = len(length)
  256. else:
  257. try:
  258. length = sp[n]
  259. L = len(length)
  260. except TypeError as err:
  261. raise nx.NetworkXError('Format of "sp" is invalid.') from err
  262. if L != order:
  263. if G.is_directed():
  264. msg = (
  265. "Found infinite path length because the digraph is not"
  266. " strongly connected"
  267. )
  268. else:
  269. msg = "Found infinite path length because the graph is not" " connected"
  270. raise nx.NetworkXError(msg)
  271. e[n] = max(length.values())
  272. if v in G:
  273. return e[v] # return single value
  274. return e
  275. def diameter(G, e=None, usebounds=False, weight=None):
  276. """Returns the diameter of the graph G.
  277. The diameter is the maximum eccentricity.
  278. Parameters
  279. ----------
  280. G : NetworkX graph
  281. A graph
  282. e : eccentricity dictionary, optional
  283. A precomputed dictionary of eccentricities.
  284. weight : string, function, or None
  285. If this is a string, then edge weights will be accessed via the
  286. edge attribute with this key (that is, the weight of the edge
  287. joining `u` to `v` will be ``G.edges[u, v][weight]``). If no
  288. such edge attribute exists, the weight of the edge is assumed to
  289. be one.
  290. If this is a function, the weight of an edge is the value
  291. returned by the function. The function must accept exactly three
  292. positional arguments: the two endpoints of an edge and the
  293. dictionary of edge attributes for that edge. The function must
  294. return a number.
  295. If this is None, every edge has weight/distance/cost 1.
  296. Weights stored as floating point values can lead to small round-off
  297. errors in distances. Use integer weights to avoid this.
  298. Weights should be positive, since they are distances.
  299. Returns
  300. -------
  301. d : integer
  302. Diameter of graph
  303. Examples
  304. --------
  305. >>> G = nx.Graph([(1, 2), (1, 3), (1, 4), (3, 4), (3, 5), (4, 5)])
  306. >>> nx.diameter(G)
  307. 3
  308. See Also
  309. --------
  310. eccentricity
  311. """
  312. if usebounds is True and e is None and not G.is_directed():
  313. return _extrema_bounding(G, compute="diameter", weight=weight)
  314. if e is None:
  315. e = eccentricity(G, weight=weight)
  316. return max(e.values())
  317. def periphery(G, e=None, usebounds=False, weight=None):
  318. """Returns the periphery of the graph G.
  319. The periphery is the set of nodes with eccentricity equal to the diameter.
  320. Parameters
  321. ----------
  322. G : NetworkX graph
  323. A graph
  324. e : eccentricity dictionary, optional
  325. A precomputed dictionary of eccentricities.
  326. weight : string, function, or None
  327. If this is a string, then edge weights will be accessed via the
  328. edge attribute with this key (that is, the weight of the edge
  329. joining `u` to `v` will be ``G.edges[u, v][weight]``). If no
  330. such edge attribute exists, the weight of the edge is assumed to
  331. be one.
  332. If this is a function, the weight of an edge is the value
  333. returned by the function. The function must accept exactly three
  334. positional arguments: the two endpoints of an edge and the
  335. dictionary of edge attributes for that edge. The function must
  336. return a number.
  337. If this is None, every edge has weight/distance/cost 1.
  338. Weights stored as floating point values can lead to small round-off
  339. errors in distances. Use integer weights to avoid this.
  340. Weights should be positive, since they are distances.
  341. Returns
  342. -------
  343. p : list
  344. List of nodes in periphery
  345. Examples
  346. --------
  347. >>> G = nx.Graph([(1, 2), (1, 3), (1, 4), (3, 4), (3, 5), (4, 5)])
  348. >>> nx.periphery(G)
  349. [2, 5]
  350. See Also
  351. --------
  352. barycenter
  353. center
  354. """
  355. if usebounds is True and e is None and not G.is_directed():
  356. return _extrema_bounding(G, compute="periphery", weight=weight)
  357. if e is None:
  358. e = eccentricity(G, weight=weight)
  359. diameter = max(e.values())
  360. p = [v for v in e if e[v] == diameter]
  361. return p
  362. def radius(G, e=None, usebounds=False, weight=None):
  363. """Returns the radius of the graph G.
  364. The radius is the minimum eccentricity.
  365. Parameters
  366. ----------
  367. G : NetworkX graph
  368. A graph
  369. e : eccentricity dictionary, optional
  370. A precomputed dictionary of eccentricities.
  371. weight : string, function, or None
  372. If this is a string, then edge weights will be accessed via the
  373. edge attribute with this key (that is, the weight of the edge
  374. joining `u` to `v` will be ``G.edges[u, v][weight]``). If no
  375. such edge attribute exists, the weight of the edge is assumed to
  376. be one.
  377. If this is a function, the weight of an edge is the value
  378. returned by the function. The function must accept exactly three
  379. positional arguments: the two endpoints of an edge and the
  380. dictionary of edge attributes for that edge. The function must
  381. return a number.
  382. If this is None, every edge has weight/distance/cost 1.
  383. Weights stored as floating point values can lead to small round-off
  384. errors in distances. Use integer weights to avoid this.
  385. Weights should be positive, since they are distances.
  386. Returns
  387. -------
  388. r : integer
  389. Radius of graph
  390. Examples
  391. --------
  392. >>> G = nx.Graph([(1, 2), (1, 3), (1, 4), (3, 4), (3, 5), (4, 5)])
  393. >>> nx.radius(G)
  394. 2
  395. """
  396. if usebounds is True and e is None and not G.is_directed():
  397. return _extrema_bounding(G, compute="radius", weight=weight)
  398. if e is None:
  399. e = eccentricity(G, weight=weight)
  400. return min(e.values())
  401. def center(G, e=None, usebounds=False, weight=None):
  402. """Returns the center of the graph G.
  403. The center is the set of nodes with eccentricity equal to radius.
  404. Parameters
  405. ----------
  406. G : NetworkX graph
  407. A graph
  408. e : eccentricity dictionary, optional
  409. A precomputed dictionary of eccentricities.
  410. weight : string, function, or None
  411. If this is a string, then edge weights will be accessed via the
  412. edge attribute with this key (that is, the weight of the edge
  413. joining `u` to `v` will be ``G.edges[u, v][weight]``). If no
  414. such edge attribute exists, the weight of the edge is assumed to
  415. be one.
  416. If this is a function, the weight of an edge is the value
  417. returned by the function. The function must accept exactly three
  418. positional arguments: the two endpoints of an edge and the
  419. dictionary of edge attributes for that edge. The function must
  420. return a number.
  421. If this is None, every edge has weight/distance/cost 1.
  422. Weights stored as floating point values can lead to small round-off
  423. errors in distances. Use integer weights to avoid this.
  424. Weights should be positive, since they are distances.
  425. Returns
  426. -------
  427. c : list
  428. List of nodes in center
  429. Examples
  430. --------
  431. >>> G = nx.Graph([(1, 2), (1, 3), (1, 4), (3, 4), (3, 5), (4, 5)])
  432. >>> list(nx.center(G))
  433. [1, 3, 4]
  434. See Also
  435. --------
  436. barycenter
  437. periphery
  438. """
  439. if usebounds is True and e is None and not G.is_directed():
  440. return _extrema_bounding(G, compute="center", weight=weight)
  441. if e is None:
  442. e = eccentricity(G, weight=weight)
  443. radius = min(e.values())
  444. p = [v for v in e if e[v] == radius]
  445. return p
  446. def barycenter(G, weight=None, attr=None, sp=None):
  447. r"""Calculate barycenter of a connected graph, optionally with edge weights.
  448. The :dfn:`barycenter` a
  449. :func:`connected <networkx.algorithms.components.is_connected>` graph
  450. :math:`G` is the subgraph induced by the set of its nodes :math:`v`
  451. minimizing the objective function
  452. .. math::
  453. \sum_{u \in V(G)} d_G(u, v),
  454. where :math:`d_G` is the (possibly weighted) :func:`path length
  455. <networkx.algorithms.shortest_paths.generic.shortest_path_length>`.
  456. The barycenter is also called the :dfn:`median`. See [West01]_, p. 78.
  457. Parameters
  458. ----------
  459. G : :class:`networkx.Graph`
  460. The connected graph :math:`G`.
  461. weight : :class:`str`, optional
  462. Passed through to
  463. :func:`~networkx.algorithms.shortest_paths.generic.shortest_path_length`.
  464. attr : :class:`str`, optional
  465. If given, write the value of the objective function to each node's
  466. `attr` attribute. Otherwise do not store the value.
  467. sp : dict of dicts, optional
  468. All pairs shortest path lengths as a dictionary of dictionaries
  469. Returns
  470. -------
  471. list
  472. Nodes of `G` that induce the barycenter of `G`.
  473. Raises
  474. ------
  475. NetworkXNoPath
  476. If `G` is disconnected. `G` may appear disconnected to
  477. :func:`barycenter` if `sp` is given but is missing shortest path
  478. lengths for any pairs.
  479. ValueError
  480. If `sp` and `weight` are both given.
  481. Examples
  482. --------
  483. >>> G = nx.Graph([(1, 2), (1, 3), (1, 4), (3, 4), (3, 5), (4, 5)])
  484. >>> nx.barycenter(G)
  485. [1, 3, 4]
  486. See Also
  487. --------
  488. center
  489. periphery
  490. """
  491. if sp is None:
  492. sp = nx.shortest_path_length(G, weight=weight)
  493. else:
  494. sp = sp.items()
  495. if weight is not None:
  496. raise ValueError("Cannot use both sp, weight arguments together")
  497. smallest, barycenter_vertices, n = float("inf"), [], len(G)
  498. for v, dists in sp:
  499. if len(dists) < n:
  500. raise nx.NetworkXNoPath(
  501. f"Input graph {G} is disconnected, so every induced subgraph "
  502. "has infinite barycentricity."
  503. )
  504. barycentricity = sum(dists.values())
  505. if attr is not None:
  506. G.nodes[v][attr] = barycentricity
  507. if barycentricity < smallest:
  508. smallest = barycentricity
  509. barycenter_vertices = [v]
  510. elif barycentricity == smallest:
  511. barycenter_vertices.append(v)
  512. return barycenter_vertices
  513. def _count_lu_permutations(perm_array):
  514. """Counts the number of permutations in SuperLU perm_c or perm_r"""
  515. perm_cnt = 0
  516. arr = perm_array.tolist()
  517. for i in range(len(arr)):
  518. if i != arr[i]:
  519. perm_cnt += 1
  520. n = arr.index(i)
  521. arr[n] = arr[i]
  522. arr[i] = i
  523. return perm_cnt
  524. @not_implemented_for("directed")
  525. def resistance_distance(G, nodeA, nodeB, weight=None, invert_weight=True):
  526. """Returns the resistance distance between node A and node B on graph G.
  527. The resistance distance between two nodes of a graph is akin to treating
  528. the graph as a grid of resistorses with a resistance equal to the provided
  529. weight.
  530. If weight is not provided, then a weight of 1 is used for all edges.
  531. Parameters
  532. ----------
  533. G : NetworkX graph
  534. A graph
  535. nodeA : node
  536. A node within graph G.
  537. nodeB : node
  538. A node within graph G, exclusive of Node A.
  539. weight : string or None, optional (default=None)
  540. The edge data key used to compute the resistance distance.
  541. If None, then each edge has weight 1.
  542. invert_weight : boolean (default=True)
  543. Proper calculation of resistance distance requires building the
  544. Laplacian matrix with the reciprocal of the weight. Not required
  545. if the weight is already inverted. Weight cannot be zero.
  546. Returns
  547. -------
  548. rd : float
  549. Value of effective resistance distance
  550. Examples
  551. --------
  552. >>> G = nx.Graph([(1, 2), (1, 3), (1, 4), (3, 4), (3, 5), (4, 5)])
  553. >>> nx.resistance_distance(G, 1, 3)
  554. 0.625
  555. Notes
  556. -----
  557. Overviews are provided in [1]_ and [2]_. Additional details on computational
  558. methods, proofs of properties, and corresponding MATLAB codes are provided
  559. in [3]_.
  560. References
  561. ----------
  562. .. [1] Wikipedia
  563. "Resistance distance."
  564. https://en.wikipedia.org/wiki/Resistance_distance
  565. .. [2] E. W. Weisstein
  566. "Resistance Distance."
  567. MathWorld--A Wolfram Web Resource
  568. https://mathworld.wolfram.com/ResistanceDistance.html
  569. .. [3] V. S. S. Vos,
  570. "Methods for determining the effective resistance."
  571. Mestrado, Mathematisch Instituut Universiteit Leiden, 2016
  572. https://www.universiteitleiden.nl/binaries/content/assets/science/mi/scripties/master/vos_vaya_master.pdf
  573. """
  574. import numpy as np
  575. import scipy as sp
  576. import scipy.sparse.linalg # call as sp.sparse.linalg
  577. if not nx.is_connected(G):
  578. msg = "Graph G must be strongly connected."
  579. raise nx.NetworkXError(msg)
  580. elif nodeA not in G:
  581. msg = "Node A is not in graph G."
  582. raise nx.NetworkXError(msg)
  583. elif nodeB not in G:
  584. msg = "Node B is not in graph G."
  585. raise nx.NetworkXError(msg)
  586. elif nodeA == nodeB:
  587. msg = "Node A and Node B cannot be the same."
  588. raise nx.NetworkXError(msg)
  589. G = G.copy()
  590. node_list = list(G)
  591. if invert_weight and weight is not None:
  592. if G.is_multigraph():
  593. for u, v, k, d in G.edges(keys=True, data=True):
  594. d[weight] = 1 / d[weight]
  595. else:
  596. for u, v, d in G.edges(data=True):
  597. d[weight] = 1 / d[weight]
  598. # Replace with collapsing topology or approximated zero?
  599. # Using determinants to compute the effective resistance is more memory
  600. # efficient than directly calculating the pseudo-inverse
  601. L = nx.laplacian_matrix(G, node_list, weight=weight).asformat("csc")
  602. indices = list(range(L.shape[0]))
  603. # w/ nodeA removed
  604. indices.remove(node_list.index(nodeA))
  605. L_a = L[indices, :][:, indices]
  606. # Both nodeA and nodeB removed
  607. indices.remove(node_list.index(nodeB))
  608. L_ab = L[indices, :][:, indices]
  609. # Factorize Laplacian submatrixes and extract diagonals
  610. # Order the diagonals to minimize the likelihood over overflows
  611. # during computing the determinant
  612. lu_a = sp.sparse.linalg.splu(L_a, options={"SymmetricMode": True})
  613. LdiagA = lu_a.U.diagonal()
  614. LdiagA_s = np.product(np.sign(LdiagA)) * np.product(lu_a.L.diagonal())
  615. LdiagA_s *= (-1) ** _count_lu_permutations(lu_a.perm_r)
  616. LdiagA_s *= (-1) ** _count_lu_permutations(lu_a.perm_c)
  617. LdiagA = np.absolute(LdiagA)
  618. LdiagA = np.sort(LdiagA)
  619. lu_ab = sp.sparse.linalg.splu(L_ab, options={"SymmetricMode": True})
  620. LdiagAB = lu_ab.U.diagonal()
  621. LdiagAB_s = np.product(np.sign(LdiagAB)) * np.product(lu_ab.L.diagonal())
  622. LdiagAB_s *= (-1) ** _count_lu_permutations(lu_ab.perm_r)
  623. LdiagAB_s *= (-1) ** _count_lu_permutations(lu_ab.perm_c)
  624. LdiagAB = np.absolute(LdiagAB)
  625. LdiagAB = np.sort(LdiagAB)
  626. # Calculate the ratio of determinant, rd = det(L_ab)/det(L_a)
  627. Ldet = np.product(np.divide(np.append(LdiagAB, [1]), LdiagA))
  628. rd = Ldet * LdiagAB_s / LdiagA_s
  629. return rd