geometric.py 39 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037
  1. """Generators for geometric graphs."""
  2. import math
  3. from bisect import bisect_left
  4. from itertools import accumulate, combinations, product
  5. import networkx as nx
  6. from networkx.utils import py_random_state
  7. __all__ = [
  8. "geometric_edges",
  9. "geographical_threshold_graph",
  10. "navigable_small_world_graph",
  11. "random_geometric_graph",
  12. "soft_random_geometric_graph",
  13. "thresholded_random_geometric_graph",
  14. "waxman_graph",
  15. "geometric_soft_configuration_graph",
  16. ]
  17. @nx._dispatchable(node_attrs="pos_name")
  18. def geometric_edges(G, radius, p=2, *, pos_name="pos"):
  19. """Returns edge list of node pairs within `radius` of each other.
  20. Parameters
  21. ----------
  22. G : networkx graph
  23. The graph from which to generate the edge list. The nodes in `G` should
  24. have an attribute ``pos`` corresponding to the node position, which is
  25. used to compute the distance to other nodes.
  26. radius : scalar
  27. The distance threshold. Edges are included in the edge list if the
  28. distance between the two nodes is less than `radius`.
  29. pos_name : string, default="pos"
  30. The name of the node attribute which represents the position of each
  31. node in 2D coordinates. Every node in the Graph must have this attribute.
  32. p : scalar, default=2
  33. The `Minkowski distance metric
  34. <https://en.wikipedia.org/wiki/Minkowski_distance>`_ used to compute
  35. distances. The default value is 2, i.e. Euclidean distance.
  36. Returns
  37. -------
  38. edges : list
  39. List of edges whose distances are less than `radius`
  40. Notes
  41. -----
  42. Radius uses Minkowski distance metric `p`.
  43. If scipy is available, `scipy.spatial.cKDTree` is used to speed computation.
  44. Examples
  45. --------
  46. Create a graph with nodes that have a "pos" attribute representing 2D
  47. coordinates.
  48. >>> G = nx.Graph()
  49. >>> G.add_nodes_from(
  50. ... [
  51. ... (0, {"pos": (0, 0)}),
  52. ... (1, {"pos": (3, 0)}),
  53. ... (2, {"pos": (8, 0)}),
  54. ... ]
  55. ... )
  56. >>> nx.geometric_edges(G, radius=1)
  57. []
  58. >>> nx.geometric_edges(G, radius=4)
  59. [(0, 1)]
  60. >>> nx.geometric_edges(G, radius=6)
  61. [(0, 1), (1, 2)]
  62. >>> nx.geometric_edges(G, radius=9)
  63. [(0, 1), (0, 2), (1, 2)]
  64. """
  65. # Input validation - every node must have a "pos" attribute
  66. for n, pos in G.nodes(data=pos_name):
  67. if pos is None:
  68. raise nx.NetworkXError(
  69. f"Node {n} (and all nodes) must have a '{pos_name}' attribute."
  70. )
  71. # NOTE: See _geometric_edges for the actual implementation. The reason this
  72. # is split into two functions is to avoid the overhead of input validation
  73. # every time the function is called internally in one of the other
  74. # geometric generators
  75. return _geometric_edges(G, radius, p, pos_name)
  76. def _geometric_edges(G, radius, p, pos_name):
  77. """
  78. Implements `geometric_edges` without input validation. See `geometric_edges`
  79. for complete docstring.
  80. """
  81. nodes_pos = G.nodes(data=pos_name)
  82. try:
  83. import scipy as sp
  84. except ImportError:
  85. # no scipy KDTree so compute by for-loop
  86. radius_p = radius**p
  87. edges = [
  88. (u, v)
  89. for (u, pu), (v, pv) in combinations(nodes_pos, 2)
  90. if sum(abs(a - b) ** p for a, b in zip(pu, pv)) <= radius_p
  91. ]
  92. return edges
  93. # scipy KDTree is available
  94. nodes, coords = list(zip(*nodes_pos))
  95. kdtree = sp.spatial.cKDTree(coords) # Cannot provide generator.
  96. edge_indexes = kdtree.query_pairs(radius, p)
  97. edges = [(nodes[u], nodes[v]) for u, v in sorted(edge_indexes)]
  98. return edges
  99. @py_random_state(5)
  100. @nx._dispatchable(graphs=None, returns_graph=True)
  101. def random_geometric_graph(
  102. n, radius, dim=2, pos=None, p=2, seed=None, *, pos_name="pos"
  103. ):
  104. """Returns a random geometric graph in the unit cube of dimensions `dim`.
  105. The random geometric graph model places `n` nodes uniformly at
  106. random in the unit cube. Two nodes are joined by an edge if the
  107. distance between the nodes is at most `radius`.
  108. Edges are determined using a KDTree when SciPy is available.
  109. This reduces the time complexity from $O(n^2)$ to $O(n)$.
  110. Parameters
  111. ----------
  112. n : int or iterable
  113. Number of nodes or iterable of nodes
  114. radius: float
  115. Distance threshold value
  116. dim : int, optional
  117. Dimension of graph
  118. pos : dict, optional
  119. A dictionary keyed by node with node positions as values.
  120. p : float, optional
  121. Which Minkowski distance metric to use. `p` has to meet the condition
  122. ``1 <= p <= infinity``.
  123. If this argument is not specified, the :math:`L^2` metric
  124. (the Euclidean distance metric), p = 2 is used.
  125. This should not be confused with the `p` of an Erdős-Rényi random
  126. graph, which represents probability.
  127. seed : integer, random_state, or None (default)
  128. Indicator of random number generation state.
  129. See :ref:`Randomness<randomness>`.
  130. pos_name : string, default="pos"
  131. The name of the node attribute which represents the position
  132. in 2D coordinates of the node in the returned graph.
  133. Returns
  134. -------
  135. Graph
  136. A random geometric graph, undirected and without self-loops.
  137. Each node has a node attribute ``'pos'`` that stores the
  138. position of that node in Euclidean space as provided by the
  139. ``pos`` keyword argument or, if ``pos`` was not provided, as
  140. generated by this function.
  141. Examples
  142. --------
  143. Create a random geometric graph on twenty nodes where nodes are joined by
  144. an edge if their distance is at most 0.1::
  145. >>> G = nx.random_geometric_graph(20, 0.1)
  146. Notes
  147. -----
  148. This uses a *k*-d tree to build the graph.
  149. The `pos` keyword argument can be used to specify node positions so you
  150. can create an arbitrary distribution and domain for positions.
  151. For example, to use a 2D Gaussian distribution of node positions with mean
  152. (0, 0) and standard deviation 2::
  153. >>> import random
  154. >>> n = 20
  155. >>> pos = {i: (random.gauss(0, 2), random.gauss(0, 2)) for i in range(n)}
  156. >>> G = nx.random_geometric_graph(n, 0.2, pos=pos)
  157. References
  158. ----------
  159. .. [1] Penrose, Mathew, *Random Geometric Graphs*,
  160. Oxford Studies in Probability, 5, 2003.
  161. """
  162. # TODO Is this function just a special case of the geographical
  163. # threshold graph?
  164. #
  165. # half_radius = {v: radius / 2 for v in n}
  166. # return geographical_threshold_graph(nodes, theta=1, alpha=1,
  167. # weight=half_radius)
  168. #
  169. G = nx.empty_graph(n)
  170. # If no positions are provided, choose uniformly random vectors in
  171. # Euclidean space of the specified dimension.
  172. if pos is None:
  173. pos = {v: [seed.random() for i in range(dim)] for v in G}
  174. nx.set_node_attributes(G, pos, pos_name)
  175. G.add_edges_from(_geometric_edges(G, radius, p, pos_name))
  176. return G
  177. @py_random_state(6)
  178. @nx._dispatchable(graphs=None, returns_graph=True)
  179. def soft_random_geometric_graph(
  180. n, radius, dim=2, pos=None, p=2, p_dist=None, seed=None, *, pos_name="pos"
  181. ):
  182. r"""Returns a soft random geometric graph in the unit cube.
  183. The soft random geometric graph [1] model places `n` nodes uniformly at
  184. random in the unit cube in dimension `dim`. Two nodes of distance, `dist`,
  185. computed by the `p`-Minkowski distance metric are joined by an edge with
  186. probability `p_dist` if the computed distance metric value of the nodes
  187. is at most `radius`, otherwise they are not joined.
  188. Edges within `radius` of each other are determined using a KDTree when
  189. SciPy is available. This reduces the time complexity from :math:`O(n^2)`
  190. to :math:`O(n)`.
  191. Parameters
  192. ----------
  193. n : int or iterable
  194. Number of nodes or iterable of nodes
  195. radius: float
  196. Distance threshold value
  197. dim : int, optional
  198. Dimension of graph
  199. pos : dict, optional
  200. A dictionary keyed by node with node positions as values.
  201. p : float, optional
  202. Which Minkowski distance metric to use.
  203. `p` has to meet the condition ``1 <= p <= infinity``.
  204. If this argument is not specified, the :math:`L^2` metric
  205. (the Euclidean distance metric), p = 2 is used.
  206. This should not be confused with the `p` of an Erdős-Rényi random
  207. graph, which represents probability.
  208. p_dist : function, optional
  209. A probability density function computing the probability of
  210. connecting two nodes that are of distance, dist, computed by the
  211. Minkowski distance metric. The probability density function, `p_dist`,
  212. must be any function that takes the metric value as input
  213. and outputs a single probability value between 0-1. The `scipy.stats`
  214. package has many probability distribution functions implemented and
  215. tools for custom probability distribution definitions [2], and passing
  216. the .pdf method of `scipy.stats` distributions can be used here. If the
  217. probability function, `p_dist`, is not supplied, the default function
  218. is an exponential distribution with rate parameter :math:`\lambda=1`.
  219. seed : integer, random_state, or None (default)
  220. Indicator of random number generation state.
  221. See :ref:`Randomness<randomness>`.
  222. pos_name : string, default="pos"
  223. The name of the node attribute which represents the position
  224. in 2D coordinates of the node in the returned graph.
  225. Returns
  226. -------
  227. Graph
  228. A soft random geometric graph, undirected and without self-loops.
  229. Each node has a node attribute ``'pos'`` that stores the
  230. position of that node in Euclidean space as provided by the
  231. ``pos`` keyword argument or, if ``pos`` was not provided, as
  232. generated by this function.
  233. Notes
  234. -----
  235. This uses a *k*-d tree to build the graph.
  236. References
  237. ----------
  238. .. [1] Penrose, Mathew D. "Connectivity of soft random geometric graphs."
  239. The Annals of Applied Probability 26.2 (2016): 986-1028.
  240. Examples
  241. --------
  242. Default Graph:
  243. >>> G = nx.soft_random_geometric_graph(50, 0.2)
  244. Custom Graph:
  245. The `pos` keyword argument can be used to specify node positions so you
  246. can create an arbitrary distribution and domain for positions.
  247. The `scipy.stats` package can be used to define the probability distribution
  248. with the ``.pdf`` method used as `p_dist`.
  249. For example, create a soft random geometric graph on 100 nodes using a 2D
  250. Gaussian distribution of node positions with mean (0, 0) and standard deviation 2,
  251. where nodes are joined by an edge with probability computed from an
  252. exponential distribution with rate parameter :math:`\lambda=1` if their
  253. Euclidean distance is at most 0.2.
  254. >>> import random
  255. >>> from scipy.stats import expon
  256. >>> n = 100
  257. >>> pos = {i: (random.gauss(0, 2), random.gauss(0, 2)) for i in range(n)}
  258. >>> p_dist = lambda x: expon.pdf(x, scale=1)
  259. >>> G = nx.soft_random_geometric_graph(n, 0.2, pos=pos, p_dist=p_dist)
  260. """
  261. G = nx.empty_graph(n)
  262. G.name = f"soft_random_geometric_graph({n}, {radius}, {dim})"
  263. # If no positions are provided, choose uniformly random vectors in
  264. # Euclidean space of the specified dimension.
  265. if pos is None:
  266. pos = {v: [seed.random() for i in range(dim)] for v in G}
  267. nx.set_node_attributes(G, pos, pos_name)
  268. # if p_dist function not supplied the default function is an exponential
  269. # distribution with rate parameter :math:`\lambda=1`.
  270. if p_dist is None:
  271. def p_dist(dist):
  272. return math.exp(-dist)
  273. def should_join(edge):
  274. u, v = edge
  275. dist = (sum(abs(a - b) ** p for a, b in zip(pos[u], pos[v]))) ** (1 / p)
  276. return seed.random() < p_dist(dist)
  277. G.add_edges_from(filter(should_join, _geometric_edges(G, radius, p, pos_name)))
  278. return G
  279. @py_random_state(7)
  280. @nx._dispatchable(graphs=None, returns_graph=True)
  281. def geographical_threshold_graph(
  282. n,
  283. theta,
  284. dim=2,
  285. pos=None,
  286. weight=None,
  287. metric=None,
  288. p_dist=None,
  289. seed=None,
  290. *,
  291. pos_name="pos",
  292. weight_name="weight",
  293. ):
  294. r"""Returns a geographical threshold graph.
  295. The geographical threshold graph model places $n$ nodes uniformly at
  296. random in a rectangular domain. Each node $u$ is assigned a weight
  297. $w_u$. Two nodes $u$ and $v$ are joined by an edge if
  298. .. math::
  299. (w_u + w_v)p_{dist}(r) \ge \theta
  300. where `r` is the distance between `u` and `v`, `p_dist` is any function of
  301. `r`, and :math:`\theta` as the threshold parameter. `p_dist` is used to
  302. give weight to the distance between nodes when deciding whether or not
  303. they should be connected. The larger `p_dist` is, the more prone nodes
  304. separated by `r` are to be connected, and vice versa.
  305. Parameters
  306. ----------
  307. n : int or iterable
  308. Number of nodes or iterable of nodes
  309. theta: float
  310. Threshold value
  311. dim : int, optional
  312. Dimension of graph
  313. pos : dict
  314. Node positions as a dictionary of tuples keyed by node.
  315. weight : dict
  316. Node weights as a dictionary of numbers keyed by node.
  317. metric : function
  318. A metric on vectors of numbers (represented as lists or
  319. tuples). This must be a function that accepts two lists (or
  320. tuples) as input and yields a number as output. The function
  321. must also satisfy the four requirements of a `metric`_.
  322. Specifically, if $d$ is the function and $x$, $y$,
  323. and $z$ are vectors in the graph, then $d$ must satisfy
  324. 1. $d(x, y) \ge 0$,
  325. 2. $d(x, y) = 0$ if and only if $x = y$,
  326. 3. $d(x, y) = d(y, x)$,
  327. 4. $d(x, z) \le d(x, y) + d(y, z)$.
  328. If this argument is not specified, the Euclidean distance metric is
  329. used.
  330. .. _metric: https://en.wikipedia.org/wiki/Metric_%28mathematics%29
  331. p_dist : function, optional
  332. Any function used to give weight to the distance between nodes when
  333. deciding whether or not they should be connected. `p_dist` was
  334. originally conceived as a probability density function giving the
  335. probability of connecting two nodes that are of metric distance `r`
  336. apart. The implementation here allows for more arbitrary definitions
  337. of `p_dist` that do not need to correspond to valid probability
  338. density functions. The :mod:`scipy.stats` package has many
  339. probability density functions implemented and tools for custom
  340. probability density definitions, and passing the ``.pdf`` method of
  341. `scipy.stats` distributions can be used here. If ``p_dist=None``
  342. (the default), the exponential function :math:`r^{-2}` is used.
  343. seed : integer, random_state, or None (default)
  344. Indicator of random number generation state.
  345. See :ref:`Randomness<randomness>`.
  346. pos_name : string, default="pos"
  347. The name of the node attribute which represents the position
  348. in 2D coordinates of the node in the returned graph.
  349. weight_name : string, default="weight"
  350. The name of the node attribute which represents the weight
  351. of the node in the returned graph.
  352. Returns
  353. -------
  354. Graph
  355. A random geographic threshold graph, undirected and without
  356. self-loops.
  357. Each node has a node attribute ``pos`` that stores the
  358. position of that node in Euclidean space as provided by the
  359. ``pos`` keyword argument or, if ``pos`` was not provided, as
  360. generated by this function. Similarly, each node has a node
  361. attribute ``weight`` that stores the weight of that node as
  362. provided or as generated.
  363. Examples
  364. --------
  365. Specify an alternate distance metric using the ``metric`` keyword
  366. argument. For example, to use the `taxicab metric`_ instead of the
  367. default `Euclidean metric`_::
  368. >>> dist = lambda x, y: sum(abs(a - b) for a, b in zip(x, y))
  369. >>> G = nx.geographical_threshold_graph(10, 0.1, metric=dist)
  370. .. _taxicab metric: https://en.wikipedia.org/wiki/Taxicab_geometry
  371. .. _Euclidean metric: https://en.wikipedia.org/wiki/Euclidean_distance
  372. Notes
  373. -----
  374. If weights are not specified they are assigned to nodes by drawing randomly
  375. from the exponential distribution with rate parameter $\lambda=1$.
  376. To specify weights from a different distribution, use the `weight` keyword
  377. argument::
  378. >>> import random
  379. >>> n = 20
  380. >>> w = {i: random.expovariate(5.0) for i in range(n)}
  381. >>> G = nx.geographical_threshold_graph(20, 50, weight=w)
  382. If node positions are not specified they are randomly assigned from the
  383. uniform distribution.
  384. References
  385. ----------
  386. .. [1] Masuda, N., Miwa, H., Konno, N.:
  387. Geographical threshold graphs with small-world and scale-free
  388. properties.
  389. Physical Review E 71, 036108 (2005)
  390. .. [2] Milan Bradonjić, Aric Hagberg and Allon G. Percus,
  391. Giant component and connectivity in geographical threshold graphs,
  392. in Algorithms and Models for the Web-Graph (WAW 2007),
  393. Antony Bonato and Fan Chung (Eds), pp. 209--216, 2007
  394. """
  395. G = nx.empty_graph(n)
  396. # If no weights are provided, choose them from an exponential
  397. # distribution.
  398. if weight is None:
  399. weight = {v: seed.expovariate(1) for v in G}
  400. # If no positions are provided, choose uniformly random vectors in
  401. # Euclidean space of the specified dimension.
  402. if pos is None:
  403. pos = {v: [seed.random() for i in range(dim)] for v in G}
  404. # If no distance metric is provided, use Euclidean distance.
  405. if metric is None:
  406. metric = math.dist
  407. nx.set_node_attributes(G, weight, weight_name)
  408. nx.set_node_attributes(G, pos, pos_name)
  409. # if p_dist is not supplied, use default r^-2
  410. if p_dist is None:
  411. def p_dist(r):
  412. return r**-2
  413. # Returns ``True`` if and only if the nodes whose attributes are
  414. # ``du`` and ``dv`` should be joined, according to the threshold
  415. # condition.
  416. def should_join(pair):
  417. u, v = pair
  418. u_pos, v_pos = pos[u], pos[v]
  419. u_weight, v_weight = weight[u], weight[v]
  420. return (u_weight + v_weight) * p_dist(metric(u_pos, v_pos)) >= theta
  421. G.add_edges_from(filter(should_join, combinations(G, 2)))
  422. return G
  423. @py_random_state(6)
  424. @nx._dispatchable(graphs=None, returns_graph=True)
  425. def waxman_graph(
  426. n,
  427. beta=0.4,
  428. alpha=0.1,
  429. L=None,
  430. domain=(0, 0, 1, 1),
  431. metric=None,
  432. seed=None,
  433. *,
  434. pos_name="pos",
  435. ):
  436. r"""Returns a Waxman random graph.
  437. The Waxman random graph model places `n` nodes uniformly at random
  438. in a rectangular domain. Each pair of nodes at distance `d` is
  439. joined by an edge with probability
  440. .. math::
  441. p = \beta \exp(-d / \alpha L).
  442. This function implements both Waxman models, using the `L` keyword
  443. argument.
  444. * Waxman-1: if `L` is not specified, it is set to be the maximum distance
  445. between any pair of nodes.
  446. * Waxman-2: if `L` is specified, the distance between a pair of nodes is
  447. chosen uniformly at random from the interval `[0, L]`.
  448. Parameters
  449. ----------
  450. n : int or iterable
  451. Number of nodes or iterable of nodes
  452. beta: float
  453. Model parameter
  454. alpha: float
  455. Model parameter
  456. L : float, optional
  457. Maximum distance between nodes. If not specified, the actual distance
  458. is calculated.
  459. domain : four-tuple of numbers, optional
  460. Domain size, given as a tuple of the form `(x_min, y_min, x_max,
  461. y_max)`.
  462. metric : function
  463. A metric on vectors of numbers (represented as lists or
  464. tuples). This must be a function that accepts two lists (or
  465. tuples) as input and yields a number as output. The function
  466. must also satisfy the four requirements of a `metric`_.
  467. Specifically, if $d$ is the function and $x$, $y$,
  468. and $z$ are vectors in the graph, then $d$ must satisfy
  469. 1. $d(x, y) \ge 0$,
  470. 2. $d(x, y) = 0$ if and only if $x = y$,
  471. 3. $d(x, y) = d(y, x)$,
  472. 4. $d(x, z) \le d(x, y) + d(y, z)$.
  473. If this argument is not specified, the Euclidean distance metric is
  474. used.
  475. .. _metric: https://en.wikipedia.org/wiki/Metric_%28mathematics%29
  476. seed : integer, random_state, or None (default)
  477. Indicator of random number generation state.
  478. See :ref:`Randomness<randomness>`.
  479. pos_name : string, default="pos"
  480. The name of the node attribute which represents the position
  481. in 2D coordinates of the node in the returned graph.
  482. Returns
  483. -------
  484. Graph
  485. A random Waxman graph, undirected and without self-loops. Each
  486. node has a node attribute ``'pos'`` that stores the position of
  487. that node in Euclidean space as generated by this function.
  488. Examples
  489. --------
  490. Specify an alternate distance metric using the ``metric`` keyword
  491. argument. For example, to use the "`taxicab metric`_" instead of the
  492. default `Euclidean metric`_::
  493. >>> dist = lambda x, y: sum(abs(a - b) for a, b in zip(x, y))
  494. >>> G = nx.waxman_graph(10, 0.5, 0.1, metric=dist)
  495. .. _taxicab metric: https://en.wikipedia.org/wiki/Taxicab_geometry
  496. .. _Euclidean metric: https://en.wikipedia.org/wiki/Euclidean_distance
  497. Notes
  498. -----
  499. Starting in NetworkX 2.0 the parameters alpha and beta align with their
  500. usual roles in the probability distribution. In earlier versions their
  501. positions in the expression were reversed. Their position in the calling
  502. sequence reversed as well to minimize backward incompatibility.
  503. References
  504. ----------
  505. .. [1] B. M. Waxman, *Routing of multipoint connections*.
  506. IEEE J. Select. Areas Commun. 6(9),(1988) 1617--1622.
  507. """
  508. G = nx.empty_graph(n)
  509. (xmin, ymin, xmax, ymax) = domain
  510. # Each node gets a uniformly random position in the given rectangle.
  511. pos = {v: (seed.uniform(xmin, xmax), seed.uniform(ymin, ymax)) for v in G}
  512. nx.set_node_attributes(G, pos, pos_name)
  513. # If no distance metric is provided, use Euclidean distance.
  514. if metric is None:
  515. metric = math.dist
  516. # If the maximum distance L is not specified (that is, we are in the
  517. # Waxman-1 model), then find the maximum distance between any pair
  518. # of nodes.
  519. #
  520. # In the Waxman-1 model, join nodes randomly based on distance. In
  521. # the Waxman-2 model, join randomly based on random l.
  522. if L is None:
  523. L = max(metric(x, y) for x, y in combinations(pos.values(), 2))
  524. def dist(u, v):
  525. return metric(pos[u], pos[v])
  526. else:
  527. def dist(u, v):
  528. return seed.random() * L
  529. # `pair` is the pair of nodes to decide whether to join.
  530. def should_join(pair):
  531. return seed.random() < beta * math.exp(-dist(*pair) / (alpha * L))
  532. G.add_edges_from(filter(should_join, combinations(G, 2)))
  533. return G
  534. @py_random_state(5)
  535. @nx._dispatchable(graphs=None, returns_graph=True)
  536. def navigable_small_world_graph(n, p=1, q=1, r=2, dim=2, seed=None):
  537. r"""Returns a navigable small-world graph.
  538. A navigable small-world graph is a directed grid with additional long-range
  539. connections that are chosen randomly.
  540. [...] we begin with a set of nodes [...] that are identified with the set
  541. of lattice points in an $n \times n$ square,
  542. $\{(i, j): i \in \{1, 2, \ldots, n\}, j \in \{1, 2, \ldots, n\}\}$,
  543. and we define the *lattice distance* between two nodes $(i, j)$ and
  544. $(k, l)$ to be the number of "lattice steps" separating them:
  545. $d((i, j), (k, l)) = |k - i| + |l - j|$.
  546. For a universal constant $p >= 1$, the node $u$ has a directed edge to
  547. every other node within lattice distance $p$---these are its *local
  548. contacts*. For universal constants $q >= 0$ and $r >= 0$ we also
  549. construct directed edges from $u$ to $q$ other nodes (the *long-range
  550. contacts*) using independent random trials; the $i$th directed edge from
  551. $u$ has endpoint $v$ with probability proportional to $[d(u,v)]^{-r}$.
  552. -- [1]_
  553. Parameters
  554. ----------
  555. n : int
  556. The length of one side of the lattice; the number of nodes in
  557. the graph is therefore $n^2$.
  558. p : int
  559. The diameter of short range connections. Each node is joined with every
  560. other node within this lattice distance.
  561. q : int
  562. The number of long-range connections for each node.
  563. r : float
  564. Exponent for decaying probability of connections. The probability of
  565. connecting to a node at lattice distance $d$ is $1/d^r$.
  566. dim : int
  567. Dimension of grid
  568. seed : integer, random_state, or None (default)
  569. Indicator of random number generation state.
  570. See :ref:`Randomness<randomness>`.
  571. References
  572. ----------
  573. .. [1] J. Kleinberg. The small-world phenomenon: An algorithmic
  574. perspective. Proc. 32nd ACM Symposium on Theory of Computing, 2000.
  575. """
  576. if p < 1:
  577. raise nx.NetworkXException("p must be >= 1")
  578. if q < 0:
  579. raise nx.NetworkXException("q must be >= 0")
  580. if r < 0:
  581. raise nx.NetworkXException("r must be >= 0")
  582. G = nx.DiGraph()
  583. nodes = list(product(range(n), repeat=dim))
  584. for p1 in nodes:
  585. probs = [0]
  586. for p2 in nodes:
  587. if p1 == p2:
  588. continue
  589. d = sum((abs(b - a) for a, b in zip(p1, p2)))
  590. if d <= p:
  591. G.add_edge(p1, p2)
  592. probs.append(d**-r)
  593. cdf = list(accumulate(probs))
  594. for _ in range(q):
  595. target = nodes[bisect_left(cdf, seed.uniform(0, cdf[-1]))]
  596. G.add_edge(p1, target)
  597. return G
  598. @py_random_state(7)
  599. @nx._dispatchable(graphs=None, returns_graph=True)
  600. def thresholded_random_geometric_graph(
  601. n,
  602. radius,
  603. theta,
  604. dim=2,
  605. pos=None,
  606. weight=None,
  607. p=2,
  608. seed=None,
  609. *,
  610. pos_name="pos",
  611. weight_name="weight",
  612. ):
  613. r"""Returns a thresholded random geometric graph in the unit cube.
  614. The thresholded random geometric graph [1] model places `n` nodes
  615. uniformly at random in the unit cube of dimensions `dim`. Each node
  616. `u` is assigned a weight :math:`w_u`. Two nodes `u` and `v` are
  617. joined by an edge if they are within the maximum connection distance,
  618. `radius` computed by the `p`-Minkowski distance and the summation of
  619. weights :math:`w_u` + :math:`w_v` is greater than or equal
  620. to the threshold parameter `theta`.
  621. Edges within `radius` of each other are determined using a KDTree when
  622. SciPy is available. This reduces the time complexity from :math:`O(n^2)`
  623. to :math:`O(n)`.
  624. Parameters
  625. ----------
  626. n : int or iterable
  627. Number of nodes or iterable of nodes
  628. radius: float
  629. Distance threshold value
  630. theta: float
  631. Threshold value
  632. dim : int, optional
  633. Dimension of graph
  634. pos : dict, optional
  635. A dictionary keyed by node with node positions as values.
  636. weight : dict, optional
  637. Node weights as a dictionary of numbers keyed by node.
  638. p : float, optional (default 2)
  639. Which Minkowski distance metric to use. `p` has to meet the condition
  640. ``1 <= p <= infinity``.
  641. If this argument is not specified, the :math:`L^2` metric
  642. (the Euclidean distance metric), p = 2 is used.
  643. This should not be confused with the `p` of an Erdős-Rényi random
  644. graph, which represents probability.
  645. seed : integer, random_state, or None (default)
  646. Indicator of random number generation state.
  647. See :ref:`Randomness<randomness>`.
  648. pos_name : string, default="pos"
  649. The name of the node attribute which represents the position
  650. in 2D coordinates of the node in the returned graph.
  651. weight_name : string, default="weight"
  652. The name of the node attribute which represents the weight
  653. of the node in the returned graph.
  654. Returns
  655. -------
  656. Graph
  657. A thresholded random geographic graph, undirected and without
  658. self-loops.
  659. Each node has a node attribute ``'pos'`` that stores the
  660. position of that node in Euclidean space as provided by the
  661. ``pos`` keyword argument or, if ``pos`` was not provided, as
  662. generated by this function. Similarly, each node has a nodethre
  663. attribute ``'weight'`` that stores the weight of that node as
  664. provided or as generated.
  665. Notes
  666. -----
  667. This uses a *k*-d tree to build the graph.
  668. References
  669. ----------
  670. .. [1] http://cole-maclean.github.io/blog/files/thesis.pdf
  671. Examples
  672. --------
  673. Default Graph:
  674. >>> G = nx.thresholded_random_geometric_graph(50, 0.2, 0.1)
  675. Custom Graph:
  676. The `pos` keyword argument can be used to specify node positions so you
  677. can create an arbitrary distribution and domain for positions.
  678. If weights are not specified they are assigned to nodes by drawing randomly
  679. from the exponential distribution with rate parameter :math:`\lambda=1`.
  680. To specify weights from a different distribution, use the `weight` keyword
  681. argument.
  682. For example, create a thresholded random geometric graph on 50 nodes using a 2D
  683. Gaussian distribution of node positions with mean (0, 0) and standard deviation 2,
  684. where nodes are joined by an edge if their sum weights drawn from
  685. a exponential distribution with rate = 5 are >= theta = 0.1 and their
  686. Euclidean distance is at most 0.2.
  687. >>> import random
  688. >>> n = 50
  689. >>> pos = {i: (random.gauss(0, 2), random.gauss(0, 2)) for i in range(n)}
  690. >>> w = {i: random.expovariate(5.0) for i in range(n)}
  691. >>> G = nx.thresholded_random_geometric_graph(n, 0.2, 0.1, 2, pos, w)
  692. """
  693. G = nx.empty_graph(n)
  694. G.name = f"thresholded_random_geometric_graph({n}, {radius}, {theta}, {dim})"
  695. # If no weights are provided, choose them from an exponential
  696. # distribution.
  697. if weight is None:
  698. weight = {v: seed.expovariate(1) for v in G}
  699. # If no positions are provided, choose uniformly random vectors in
  700. # Euclidean space of the specified dimension.
  701. if pos is None:
  702. pos = {v: [seed.random() for i in range(dim)] for v in G}
  703. # If no distance metric is provided, use Euclidean distance.
  704. nx.set_node_attributes(G, weight, weight_name)
  705. nx.set_node_attributes(G, pos, pos_name)
  706. edges = (
  707. (u, v)
  708. for u, v in _geometric_edges(G, radius, p, pos_name)
  709. if weight[u] + weight[v] >= theta
  710. )
  711. G.add_edges_from(edges)
  712. return G
  713. @py_random_state(5)
  714. @nx._dispatchable(graphs=None, returns_graph=True)
  715. def geometric_soft_configuration_graph(
  716. *, beta, n=None, gamma=None, mean_degree=None, kappas=None, seed=None
  717. ):
  718. r"""Returns a random graph from the geometric soft configuration model.
  719. The $\mathbb{S}^1$ model [1]_ is the geometric soft configuration model
  720. which is able to explain many fundamental features of real networks such as
  721. small-world property, heteregenous degree distributions, high level of
  722. clustering, and self-similarity.
  723. In the geometric soft configuration model, a node $i$ is assigned two hidden
  724. variables: a hidden degree $\kappa_i$, quantifying its popularity, influence,
  725. or importance, and an angular position $\theta_i$ in a circle abstracting the
  726. similarity space, where angular distances between nodes are a proxy for their
  727. similarity. Focusing on the angular position, this model is often called
  728. the $\mathbb{S}^1$ model (a one-dimensional sphere). The circle's radius is
  729. adjusted to $R = N/2\pi$, where $N$ is the number of nodes, so that the density
  730. is set to 1 without loss of generality.
  731. The connection probability between any pair of nodes increases with
  732. the product of their hidden degrees (i.e., their combined popularities),
  733. and decreases with the angular distance between the two nodes.
  734. Specifically, nodes $i$ and $j$ are connected with the probability
  735. $p_{ij} = \frac{1}{1 + \frac{d_{ij}^\beta}{\left(\mu \kappa_i \kappa_j\right)^{\max(1, \beta)}}}$
  736. where $d_{ij} = R\Delta\theta_{ij}$ is the arc length of the circle between
  737. nodes $i$ and $j$ separated by an angular distance $\Delta\theta_{ij}$.
  738. Parameters $\mu$ and $\beta$ (also called inverse temperature) control the
  739. average degree and the clustering coefficient, respectively.
  740. It can be shown [2]_ that the model undergoes a structural phase transition
  741. at $\beta=1$ so that for $\beta<1$ networks are unclustered in the thermodynamic
  742. limit (when $N\to \infty$) whereas for $\beta>1$ the ensemble generates
  743. networks with finite clustering coefficient.
  744. The $\mathbb{S}^1$ model can be expressed as a purely geometric model
  745. $\mathbb{H}^2$ in the hyperbolic plane [3]_ by mapping the hidden degree of
  746. each node into a radial coordinate as
  747. $r_i = \hat{R} - \frac{2 \max(1, \beta)}{\beta \zeta} \ln \left(\frac{\kappa_i}{\kappa_0}\right)$
  748. where $\hat{R}$ is the radius of the hyperbolic disk and $\zeta$ is the curvature,
  749. $\hat{R} = \frac{2}{\zeta} \ln \left(\frac{N}{\pi}\right)
  750. - \frac{2\max(1, \beta)}{\beta \zeta} \ln (\mu \kappa_0^2)$
  751. The connection probability then reads
  752. $p_{ij} = \frac{1}{1 + \exp\left({\frac{\beta\zeta}{2} (x_{ij} - \hat{R})}\right)}$
  753. where
  754. $x_{ij} = r_i + r_j + \frac{2}{\zeta} \ln \frac{\Delta\theta_{ij}}{2}$
  755. is a good approximation of the hyperbolic distance between two nodes separated
  756. by an angular distance $\Delta\theta_{ij}$ with radial coordinates $r_i$ and $r_j$.
  757. For $\beta > 1$, the curvature $\zeta = 1$, for $\beta < 1$, $\zeta = \beta^{-1}$.
  758. Parameters
  759. ----------
  760. Either `n`, `gamma`, `mean_degree` are provided or `kappas`. The values of
  761. `n`, `gamma`, `mean_degree` (if provided) are used to construct a random
  762. kappa-dict keyed by node with values sampled from a power-law distribution.
  763. beta : positive number
  764. Inverse temperature, controlling the clustering coefficient.
  765. n : int (default: None)
  766. Size of the network (number of nodes).
  767. If not provided, `kappas` must be provided and holds the nodes.
  768. gamma : float (default: None)
  769. Exponent of the power-law distribution for hidden degrees `kappas`.
  770. If not provided, `kappas` must be provided directly.
  771. mean_degree : float (default: None)
  772. The mean degree in the network.
  773. If not provided, `kappas` must be provided directly.
  774. kappas : dict (default: None)
  775. A dict keyed by node to its hidden degree value.
  776. If not provided, random values are computed based on a power-law
  777. distribution using `n`, `gamma` and `mean_degree`.
  778. seed : int, random_state, or None (default)
  779. Indicator of random number generation state.
  780. See :ref:`Randomness<randomness>`.
  781. Returns
  782. -------
  783. Graph
  784. A random geometric soft configuration graph (undirected with no self-loops).
  785. Each node has three node-attributes:
  786. - ``kappa`` that represents the hidden degree.
  787. - ``theta`` the position in the similarity space ($\mathbb{S}^1$) which is
  788. also the angular position in the hyperbolic plane.
  789. - ``radius`` the radial position in the hyperbolic plane
  790. (based on the hidden degree).
  791. Examples
  792. --------
  793. Generate a network with specified parameters:
  794. >>> G = nx.geometric_soft_configuration_graph(
  795. ... beta=1.5, n=100, gamma=2.7, mean_degree=5
  796. ... )
  797. Create a geometric soft configuration graph with 100 nodes. The $\beta$ parameter
  798. is set to 1.5 and the exponent of the powerlaw distribution of the hidden
  799. degrees is 2.7 with mean value of 5.
  800. Generate a network with predefined hidden degrees:
  801. >>> kappas = {i: 10 for i in range(100)}
  802. >>> G = nx.geometric_soft_configuration_graph(beta=2.5, kappas=kappas)
  803. Create a geometric soft configuration graph with 100 nodes. The $\beta$ parameter
  804. is set to 2.5 and all nodes with hidden degree $\kappa=10$.
  805. References
  806. ----------
  807. .. [1] Serrano, M. Á., Krioukov, D., & Boguñá, M. (2008). Self-similarity
  808. of complex networks and hidden metric spaces. Physical review letters, 100(7), 078701.
  809. .. [2] van der Kolk, J., Serrano, M. Á., & Boguñá, M. (2022). An anomalous
  810. topological phase transition in spatial random graphs. Communications Physics, 5(1), 245.
  811. .. [3] Krioukov, D., Papadopoulos, F., Kitsak, M., Vahdat, A., & Boguná, M. (2010).
  812. Hyperbolic geometry of complex networks. Physical Review E, 82(3), 036106.
  813. """
  814. if beta <= 0:
  815. raise nx.NetworkXError("The parameter beta cannot be smaller or equal to 0.")
  816. if kappas is not None:
  817. if not all((n is None, gamma is None, mean_degree is None)):
  818. raise nx.NetworkXError(
  819. "When kappas is input, n, gamma and mean_degree must not be."
  820. )
  821. n = len(kappas)
  822. mean_degree = sum(kappas) / len(kappas)
  823. else:
  824. if any((n is None, gamma is None, mean_degree is None)):
  825. raise nx.NetworkXError(
  826. "Please provide either kappas, or all 3 of: n, gamma and mean_degree."
  827. )
  828. # Generate `n` hidden degrees from a powerlaw distribution
  829. # with given exponent `gamma` and mean value `mean_degree`
  830. gam_ratio = (gamma - 2) / (gamma - 1)
  831. kappa_0 = mean_degree * gam_ratio * (1 - 1 / n) / (1 - 1 / n**gam_ratio)
  832. base = 1 - 1 / n
  833. power = 1 / (1 - gamma)
  834. kappas = {i: kappa_0 * (1 - seed.random() * base) ** power for i in range(n)}
  835. G = nx.Graph()
  836. R = n / (2 * math.pi)
  837. # Approximate values for mu in the thermodynamic limit (when n -> infinity)
  838. if beta > 1:
  839. mu = beta * math.sin(math.pi / beta) / (2 * math.pi * mean_degree)
  840. elif beta == 1:
  841. mu = 1 / (2 * mean_degree * math.log(n))
  842. else:
  843. mu = (1 - beta) / (2**beta * mean_degree * n ** (1 - beta))
  844. # Generate random positions on a circle
  845. thetas = {k: seed.uniform(0, 2 * math.pi) for k in kappas}
  846. for u in kappas:
  847. for v in list(G):
  848. angle = math.pi - math.fabs(math.pi - math.fabs(thetas[u] - thetas[v]))
  849. dij = math.pow(R * angle, beta)
  850. mu_kappas = math.pow(mu * kappas[u] * kappas[v], max(1, beta))
  851. p_ij = 1 / (1 + dij / mu_kappas)
  852. # Create an edge with a certain connection probability
  853. if seed.random() < p_ij:
  854. G.add_edge(u, v)
  855. G.add_node(u)
  856. nx.set_node_attributes(G, thetas, "theta")
  857. nx.set_node_attributes(G, kappas, "kappa")
  858. # Map hidden degrees into the radial coordinates
  859. zeta = 1 if beta > 1 else 1 / beta
  860. kappa_min = min(kappas.values())
  861. R_c = 2 * max(1, beta) / (beta * zeta)
  862. R_hat = (2 / zeta) * math.log(n / math.pi) - R_c * math.log(mu * kappa_min)
  863. radii = {node: R_hat - R_c * math.log(kappa) for node, kappa in kappas.items()}
  864. nx.set_node_attributes(G, radii, "radius")
  865. return G