geometric.py 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786
  1. """Generators for geometric graphs.
  2. """
  3. import math
  4. from bisect import bisect_left
  5. from itertools import accumulate, combinations, product
  6. import networkx as nx
  7. from networkx.utils import py_random_state
  8. __all__ = [
  9. "geometric_edges",
  10. "geographical_threshold_graph",
  11. "navigable_small_world_graph",
  12. "random_geometric_graph",
  13. "soft_random_geometric_graph",
  14. "thresholded_random_geometric_graph",
  15. "waxman_graph",
  16. ]
  17. def geometric_edges(G, radius, p=2):
  18. """Returns edge list of node pairs within `radius` of each other.
  19. Parameters
  20. ----------
  21. G : networkx graph
  22. The graph from which to generate the edge list. The nodes in `G` should
  23. have an attribute ``pos`` corresponding to the node position, which is
  24. used to compute the distance to other nodes.
  25. radius : scalar
  26. The distance threshold. Edges are included in the edge list if the
  27. distance between the two nodes is less than `radius`.
  28. p : scalar, default=2
  29. The `Minkowski distance metric
  30. <https://en.wikipedia.org/wiki/Minkowski_distance>`_ used to compute
  31. distances. The default value is 2, i.e. Euclidean distance.
  32. Returns
  33. -------
  34. edges : list
  35. List of edges whose distances are less than `radius`
  36. Notes
  37. -----
  38. Radius uses Minkowski distance metric `p`.
  39. If scipy is available, `scipy.spatial.cKDTree` is used to speed computation.
  40. Examples
  41. --------
  42. Create a graph with nodes that have a "pos" attribute representing 2D
  43. coordinates.
  44. >>> G = nx.Graph()
  45. >>> G.add_nodes_from([
  46. ... (0, {"pos": (0, 0)}),
  47. ... (1, {"pos": (3, 0)}),
  48. ... (2, {"pos": (8, 0)}),
  49. ... ])
  50. >>> nx.geometric_edges(G, radius=1)
  51. []
  52. >>> nx.geometric_edges(G, radius=4)
  53. [(0, 1)]
  54. >>> nx.geometric_edges(G, radius=6)
  55. [(0, 1), (1, 2)]
  56. >>> nx.geometric_edges(G, radius=9)
  57. [(0, 1), (0, 2), (1, 2)]
  58. """
  59. # Input validation - every node must have a "pos" attribute
  60. for n, pos in G.nodes(data="pos"):
  61. if pos is None:
  62. raise nx.NetworkXError(
  63. f"All nodes in `G` must have a 'pos' attribute. Check node {n}"
  64. )
  65. # NOTE: See _geometric_edges for the actual implementation. The reason this
  66. # is split into two functions is to avoid the overhead of input validation
  67. # every time the function is called internally in one of the other
  68. # geometric generators
  69. return _geometric_edges(G, radius, p)
  70. def _geometric_edges(G, radius, p=2):
  71. """
  72. Implements `geometric_edges` without input validation. See `geometric_edges`
  73. for complete docstring.
  74. """
  75. nodes_pos = G.nodes(data="pos")
  76. try:
  77. import scipy as sp
  78. import scipy.spatial # call as sp.spatial
  79. except ImportError:
  80. # no scipy KDTree so compute by for-loop
  81. radius_p = radius**p
  82. edges = [
  83. (u, v)
  84. for (u, pu), (v, pv) in combinations(nodes_pos, 2)
  85. if sum(abs(a - b) ** p for a, b in zip(pu, pv)) <= radius_p
  86. ]
  87. return edges
  88. # scipy KDTree is available
  89. nodes, coords = list(zip(*nodes_pos))
  90. kdtree = sp.spatial.cKDTree(coords) # Cannot provide generator.
  91. edge_indexes = kdtree.query_pairs(radius, p)
  92. edges = [(nodes[u], nodes[v]) for u, v in sorted(edge_indexes)]
  93. return edges
  94. @py_random_state(5)
  95. def random_geometric_graph(n, radius, dim=2, pos=None, p=2, seed=None):
  96. """Returns a random geometric graph in the unit cube of dimensions `dim`.
  97. The random geometric graph model places `n` nodes uniformly at
  98. random in the unit cube. Two nodes are joined by an edge if the
  99. distance between the nodes is at most `radius`.
  100. Edges are determined using a KDTree when SciPy is available.
  101. This reduces the time complexity from $O(n^2)$ to $O(n)$.
  102. Parameters
  103. ----------
  104. n : int or iterable
  105. Number of nodes or iterable of nodes
  106. radius: float
  107. Distance threshold value
  108. dim : int, optional
  109. Dimension of graph
  110. pos : dict, optional
  111. A dictionary keyed by node with node positions as values.
  112. p : float, optional
  113. Which Minkowski distance metric to use. `p` has to meet the condition
  114. ``1 <= p <= infinity``.
  115. If this argument is not specified, the :math:`L^2` metric
  116. (the Euclidean distance metric), p = 2 is used.
  117. This should not be confused with the `p` of an Erdős-Rényi random
  118. graph, which represents probability.
  119. seed : integer, random_state, or None (default)
  120. Indicator of random number generation state.
  121. See :ref:`Randomness<randomness>`.
  122. Returns
  123. -------
  124. Graph
  125. A random geometric graph, undirected and without self-loops.
  126. Each node has a node attribute ``'pos'`` that stores the
  127. position of that node in Euclidean space as provided by the
  128. ``pos`` keyword argument or, if ``pos`` was not provided, as
  129. generated by this function.
  130. Examples
  131. --------
  132. Create a random geometric graph on twenty nodes where nodes are joined by
  133. an edge if their distance is at most 0.1::
  134. >>> G = nx.random_geometric_graph(20, 0.1)
  135. Notes
  136. -----
  137. This uses a *k*-d tree to build the graph.
  138. The `pos` keyword argument can be used to specify node positions so you
  139. can create an arbitrary distribution and domain for positions.
  140. For example, to use a 2D Gaussian distribution of node positions with mean
  141. (0, 0) and standard deviation 2::
  142. >>> import random
  143. >>> n = 20
  144. >>> pos = {i: (random.gauss(0, 2), random.gauss(0, 2)) for i in range(n)}
  145. >>> G = nx.random_geometric_graph(n, 0.2, pos=pos)
  146. References
  147. ----------
  148. .. [1] Penrose, Mathew, *Random Geometric Graphs*,
  149. Oxford Studies in Probability, 5, 2003.
  150. """
  151. # TODO Is this function just a special case of the geographical
  152. # threshold graph?
  153. #
  154. # half_radius = {v: radius / 2 for v in n}
  155. # return geographical_threshold_graph(nodes, theta=1, alpha=1,
  156. # weight=half_radius)
  157. #
  158. G = nx.empty_graph(n)
  159. # If no positions are provided, choose uniformly random vectors in
  160. # Euclidean space of the specified dimension.
  161. if pos is None:
  162. pos = {v: [seed.random() for i in range(dim)] for v in G}
  163. nx.set_node_attributes(G, pos, "pos")
  164. G.add_edges_from(_geometric_edges(G, radius, p))
  165. return G
  166. @py_random_state(6)
  167. def soft_random_geometric_graph(
  168. n, radius, dim=2, pos=None, p=2, p_dist=None, seed=None
  169. ):
  170. r"""Returns a soft random geometric graph in the unit cube.
  171. The soft random geometric graph [1] model places `n` nodes uniformly at
  172. random in the unit cube in dimension `dim`. Two nodes of distance, `dist`,
  173. computed by the `p`-Minkowski distance metric are joined by an edge with
  174. probability `p_dist` if the computed distance metric value of the nodes
  175. is at most `radius`, otherwise they are not joined.
  176. Edges within `radius` of each other are determined using a KDTree when
  177. SciPy is available. This reduces the time complexity from :math:`O(n^2)`
  178. to :math:`O(n)`.
  179. Parameters
  180. ----------
  181. n : int or iterable
  182. Number of nodes or iterable of nodes
  183. radius: float
  184. Distance threshold value
  185. dim : int, optional
  186. Dimension of graph
  187. pos : dict, optional
  188. A dictionary keyed by node with node positions as values.
  189. p : float, optional
  190. Which Minkowski distance metric to use.
  191. `p` has to meet the condition ``1 <= p <= infinity``.
  192. If this argument is not specified, the :math:`L^2` metric
  193. (the Euclidean distance metric), p = 2 is used.
  194. This should not be confused with the `p` of an Erdős-Rényi random
  195. graph, which represents probability.
  196. p_dist : function, optional
  197. A probability density function computing the probability of
  198. connecting two nodes that are of distance, dist, computed by the
  199. Minkowski distance metric. The probability density function, `p_dist`,
  200. must be any function that takes the metric value as input
  201. and outputs a single probability value between 0-1. The scipy.stats
  202. package has many probability distribution functions implemented and
  203. tools for custom probability distribution definitions [2], and passing
  204. the .pdf method of scipy.stats distributions can be used here. If the
  205. probability function, `p_dist`, is not supplied, the default function
  206. is an exponential distribution with rate parameter :math:`\lambda=1`.
  207. seed : integer, random_state, or None (default)
  208. Indicator of random number generation state.
  209. See :ref:`Randomness<randomness>`.
  210. Returns
  211. -------
  212. Graph
  213. A soft random geometric graph, undirected and without self-loops.
  214. Each node has a node attribute ``'pos'`` that stores the
  215. position of that node in Euclidean space as provided by the
  216. ``pos`` keyword argument or, if ``pos`` was not provided, as
  217. generated by this function.
  218. Examples
  219. --------
  220. Default Graph:
  221. G = nx.soft_random_geometric_graph(50, 0.2)
  222. Custom Graph:
  223. Create a soft random geometric graph on 100 uniformly distributed nodes
  224. where nodes are joined by an edge with probability computed from an
  225. exponential distribution with rate parameter :math:`\lambda=1` if their
  226. Euclidean distance is at most 0.2.
  227. Notes
  228. -----
  229. This uses a *k*-d tree to build the graph.
  230. The `pos` keyword argument can be used to specify node positions so you
  231. can create an arbitrary distribution and domain for positions.
  232. For example, to use a 2D Gaussian distribution of node positions with mean
  233. (0, 0) and standard deviation 2
  234. The scipy.stats package can be used to define the probability distribution
  235. with the .pdf method used as `p_dist`.
  236. ::
  237. >>> import random
  238. >>> import math
  239. >>> n = 100
  240. >>> pos = {i: (random.gauss(0, 2), random.gauss(0, 2)) for i in range(n)}
  241. >>> p_dist = lambda dist: math.exp(-dist)
  242. >>> G = nx.soft_random_geometric_graph(n, 0.2, pos=pos, p_dist=p_dist)
  243. References
  244. ----------
  245. .. [1] Penrose, Mathew D. "Connectivity of soft random geometric graphs."
  246. The Annals of Applied Probability 26.2 (2016): 986-1028.
  247. .. [2] scipy.stats -
  248. https://docs.scipy.org/doc/scipy/reference/tutorial/stats.html
  249. """
  250. G = nx.empty_graph(n)
  251. G.name = f"soft_random_geometric_graph({n}, {radius}, {dim})"
  252. # If no positions are provided, choose uniformly random vectors in
  253. # Euclidean space of the specified dimension.
  254. if pos is None:
  255. pos = {v: [seed.random() for i in range(dim)] for v in G}
  256. nx.set_node_attributes(G, pos, "pos")
  257. # if p_dist function not supplied the default function is an exponential
  258. # distribution with rate parameter :math:`\lambda=1`.
  259. if p_dist is None:
  260. def p_dist(dist):
  261. return math.exp(-dist)
  262. def should_join(edge):
  263. u, v = edge
  264. dist = (sum(abs(a - b) ** p for a, b in zip(pos[u], pos[v]))) ** (1 / p)
  265. return seed.random() < p_dist(dist)
  266. G.add_edges_from(filter(should_join, _geometric_edges(G, radius, p)))
  267. return G
  268. @py_random_state(7)
  269. def geographical_threshold_graph(
  270. n, theta, dim=2, pos=None, weight=None, metric=None, p_dist=None, seed=None
  271. ):
  272. r"""Returns a geographical threshold graph.
  273. The geographical threshold graph model places $n$ nodes uniformly at
  274. random in a rectangular domain. Each node $u$ is assigned a weight
  275. $w_u$. Two nodes $u$ and $v$ are joined by an edge if
  276. .. math::
  277. (w_u + w_v)p_{dist}(r) \ge \theta
  278. where `r` is the distance between `u` and `v`, `p_dist` is any function of
  279. `r`, and :math:`\theta` as the threshold parameter. `p_dist` is used to
  280. give weight to the distance between nodes when deciding whether or not
  281. they should be connected. The larger `p_dist` is, the more prone nodes
  282. separated by `r` are to be connected, and vice versa.
  283. Parameters
  284. ----------
  285. n : int or iterable
  286. Number of nodes or iterable of nodes
  287. theta: float
  288. Threshold value
  289. dim : int, optional
  290. Dimension of graph
  291. pos : dict
  292. Node positions as a dictionary of tuples keyed by node.
  293. weight : dict
  294. Node weights as a dictionary of numbers keyed by node.
  295. metric : function
  296. A metric on vectors of numbers (represented as lists or
  297. tuples). This must be a function that accepts two lists (or
  298. tuples) as input and yields a number as output. The function
  299. must also satisfy the four requirements of a `metric`_.
  300. Specifically, if $d$ is the function and $x$, $y$,
  301. and $z$ are vectors in the graph, then $d$ must satisfy
  302. 1. $d(x, y) \ge 0$,
  303. 2. $d(x, y) = 0$ if and only if $x = y$,
  304. 3. $d(x, y) = d(y, x)$,
  305. 4. $d(x, z) \le d(x, y) + d(y, z)$.
  306. If this argument is not specified, the Euclidean distance metric is
  307. used.
  308. .. _metric: https://en.wikipedia.org/wiki/Metric_%28mathematics%29
  309. p_dist : function, optional
  310. Any function used to give weight to the distance between nodes when
  311. deciding whether or not they should be connected. `p_dist` was
  312. originally conceived as a probability density function giving the
  313. probability of connecting two nodes that are of metric distance `r`
  314. apart. The implementation here allows for more arbitrary definitions
  315. of `p_dist` that do not need to correspond to valid probability
  316. density functions. The :mod:`scipy.stats` package has many
  317. probability density functions implemented and tools for custom
  318. probability density definitions, and passing the ``.pdf`` method of
  319. scipy.stats distributions can be used here. If ``p_dist=None``
  320. (the default), the exponential function :math:`r^{-2}` is used.
  321. seed : integer, random_state, or None (default)
  322. Indicator of random number generation state.
  323. See :ref:`Randomness<randomness>`.
  324. Returns
  325. -------
  326. Graph
  327. A random geographic threshold graph, undirected and without
  328. self-loops.
  329. Each node has a node attribute ``pos`` that stores the
  330. position of that node in Euclidean space as provided by the
  331. ``pos`` keyword argument or, if ``pos`` was not provided, as
  332. generated by this function. Similarly, each node has a node
  333. attribute ``weight`` that stores the weight of that node as
  334. provided or as generated.
  335. Examples
  336. --------
  337. Specify an alternate distance metric using the ``metric`` keyword
  338. argument. For example, to use the `taxicab metric`_ instead of the
  339. default `Euclidean metric`_::
  340. >>> dist = lambda x, y: sum(abs(a - b) for a, b in zip(x, y))
  341. >>> G = nx.geographical_threshold_graph(10, 0.1, metric=dist)
  342. .. _taxicab metric: https://en.wikipedia.org/wiki/Taxicab_geometry
  343. .. _Euclidean metric: https://en.wikipedia.org/wiki/Euclidean_distance
  344. Notes
  345. -----
  346. If weights are not specified they are assigned to nodes by drawing randomly
  347. from the exponential distribution with rate parameter $\lambda=1$.
  348. To specify weights from a different distribution, use the `weight` keyword
  349. argument::
  350. >>> import random
  351. >>> n = 20
  352. >>> w = {i: random.expovariate(5.0) for i in range(n)}
  353. >>> G = nx.geographical_threshold_graph(20, 50, weight=w)
  354. If node positions are not specified they are randomly assigned from the
  355. uniform distribution.
  356. References
  357. ----------
  358. .. [1] Masuda, N., Miwa, H., Konno, N.:
  359. Geographical threshold graphs with small-world and scale-free
  360. properties.
  361. Physical Review E 71, 036108 (2005)
  362. .. [2] Milan Bradonjić, Aric Hagberg and Allon G. Percus,
  363. Giant component and connectivity in geographical threshold graphs,
  364. in Algorithms and Models for the Web-Graph (WAW 2007),
  365. Antony Bonato and Fan Chung (Eds), pp. 209--216, 2007
  366. """
  367. G = nx.empty_graph(n)
  368. # If no weights are provided, choose them from an exponential
  369. # distribution.
  370. if weight is None:
  371. weight = {v: seed.expovariate(1) for v in G}
  372. # If no positions are provided, choose uniformly random vectors in
  373. # Euclidean space of the specified dimension.
  374. if pos is None:
  375. pos = {v: [seed.random() for i in range(dim)] for v in G}
  376. # If no distance metric is provided, use Euclidean distance.
  377. if metric is None:
  378. metric = math.dist
  379. nx.set_node_attributes(G, weight, "weight")
  380. nx.set_node_attributes(G, pos, "pos")
  381. # if p_dist is not supplied, use default r^-2
  382. if p_dist is None:
  383. def p_dist(r):
  384. return r**-2
  385. # Returns ``True`` if and only if the nodes whose attributes are
  386. # ``du`` and ``dv`` should be joined, according to the threshold
  387. # condition.
  388. def should_join(pair):
  389. u, v = pair
  390. u_pos, v_pos = pos[u], pos[v]
  391. u_weight, v_weight = weight[u], weight[v]
  392. return (u_weight + v_weight) * p_dist(metric(u_pos, v_pos)) >= theta
  393. G.add_edges_from(filter(should_join, combinations(G, 2)))
  394. return G
  395. @py_random_state(6)
  396. def waxman_graph(
  397. n, beta=0.4, alpha=0.1, L=None, domain=(0, 0, 1, 1), metric=None, seed=None
  398. ):
  399. r"""Returns a Waxman random graph.
  400. The Waxman random graph model places `n` nodes uniformly at random
  401. in a rectangular domain. Each pair of nodes at distance `d` is
  402. joined by an edge with probability
  403. .. math::
  404. p = \beta \exp(-d / \alpha L).
  405. This function implements both Waxman models, using the `L` keyword
  406. argument.
  407. * Waxman-1: if `L` is not specified, it is set to be the maximum distance
  408. between any pair of nodes.
  409. * Waxman-2: if `L` is specified, the distance between a pair of nodes is
  410. chosen uniformly at random from the interval `[0, L]`.
  411. Parameters
  412. ----------
  413. n : int or iterable
  414. Number of nodes or iterable of nodes
  415. beta: float
  416. Model parameter
  417. alpha: float
  418. Model parameter
  419. L : float, optional
  420. Maximum distance between nodes. If not specified, the actual distance
  421. is calculated.
  422. domain : four-tuple of numbers, optional
  423. Domain size, given as a tuple of the form `(x_min, y_min, x_max,
  424. y_max)`.
  425. metric : function
  426. A metric on vectors of numbers (represented as lists or
  427. tuples). This must be a function that accepts two lists (or
  428. tuples) as input and yields a number as output. The function
  429. must also satisfy the four requirements of a `metric`_.
  430. Specifically, if $d$ is the function and $x$, $y$,
  431. and $z$ are vectors in the graph, then $d$ must satisfy
  432. 1. $d(x, y) \ge 0$,
  433. 2. $d(x, y) = 0$ if and only if $x = y$,
  434. 3. $d(x, y) = d(y, x)$,
  435. 4. $d(x, z) \le d(x, y) + d(y, z)$.
  436. If this argument is not specified, the Euclidean distance metric is
  437. used.
  438. .. _metric: https://en.wikipedia.org/wiki/Metric_%28mathematics%29
  439. seed : integer, random_state, or None (default)
  440. Indicator of random number generation state.
  441. See :ref:`Randomness<randomness>`.
  442. Returns
  443. -------
  444. Graph
  445. A random Waxman graph, undirected and without self-loops. Each
  446. node has a node attribute ``'pos'`` that stores the position of
  447. that node in Euclidean space as generated by this function.
  448. Examples
  449. --------
  450. Specify an alternate distance metric using the ``metric`` keyword
  451. argument. For example, to use the "`taxicab metric`_" instead of the
  452. default `Euclidean metric`_::
  453. >>> dist = lambda x, y: sum(abs(a - b) for a, b in zip(x, y))
  454. >>> G = nx.waxman_graph(10, 0.5, 0.1, metric=dist)
  455. .. _taxicab metric: https://en.wikipedia.org/wiki/Taxicab_geometry
  456. .. _Euclidean metric: https://en.wikipedia.org/wiki/Euclidean_distance
  457. Notes
  458. -----
  459. Starting in NetworkX 2.0 the parameters alpha and beta align with their
  460. usual roles in the probability distribution. In earlier versions their
  461. positions in the expression were reversed. Their position in the calling
  462. sequence reversed as well to minimize backward incompatibility.
  463. References
  464. ----------
  465. .. [1] B. M. Waxman, *Routing of multipoint connections*.
  466. IEEE J. Select. Areas Commun. 6(9),(1988) 1617--1622.
  467. """
  468. G = nx.empty_graph(n)
  469. (xmin, ymin, xmax, ymax) = domain
  470. # Each node gets a uniformly random position in the given rectangle.
  471. pos = {v: (seed.uniform(xmin, xmax), seed.uniform(ymin, ymax)) for v in G}
  472. nx.set_node_attributes(G, pos, "pos")
  473. # If no distance metric is provided, use Euclidean distance.
  474. if metric is None:
  475. metric = math.dist
  476. # If the maximum distance L is not specified (that is, we are in the
  477. # Waxman-1 model), then find the maximum distance between any pair
  478. # of nodes.
  479. #
  480. # In the Waxman-1 model, join nodes randomly based on distance. In
  481. # the Waxman-2 model, join randomly based on random l.
  482. if L is None:
  483. L = max(metric(x, y) for x, y in combinations(pos.values(), 2))
  484. def dist(u, v):
  485. return metric(pos[u], pos[v])
  486. else:
  487. def dist(u, v):
  488. return seed.random() * L
  489. # `pair` is the pair of nodes to decide whether to join.
  490. def should_join(pair):
  491. return seed.random() < beta * math.exp(-dist(*pair) / (alpha * L))
  492. G.add_edges_from(filter(should_join, combinations(G, 2)))
  493. return G
  494. @py_random_state(5)
  495. def navigable_small_world_graph(n, p=1, q=1, r=2, dim=2, seed=None):
  496. r"""Returns a navigable small-world graph.
  497. A navigable small-world graph is a directed grid with additional long-range
  498. connections that are chosen randomly.
  499. [...] we begin with a set of nodes [...] that are identified with the set
  500. of lattice points in an $n \times n$ square,
  501. $\{(i, j): i \in \{1, 2, \ldots, n\}, j \in \{1, 2, \ldots, n\}\}$,
  502. and we define the *lattice distance* between two nodes $(i, j)$ and
  503. $(k, l)$ to be the number of "lattice steps" separating them:
  504. $d((i, j), (k, l)) = |k - i| + |l - j|$.
  505. For a universal constant $p >= 1$, the node $u$ has a directed edge to
  506. every other node within lattice distance $p$---these are its *local
  507. contacts*. For universal constants $q >= 0$ and $r >= 0$ we also
  508. construct directed edges from $u$ to $q$ other nodes (the *long-range
  509. contacts*) using independent random trials; the $i$th directed edge from
  510. $u$ has endpoint $v$ with probability proportional to $[d(u,v)]^{-r}$.
  511. -- [1]_
  512. Parameters
  513. ----------
  514. n : int
  515. The length of one side of the lattice; the number of nodes in
  516. the graph is therefore $n^2$.
  517. p : int
  518. The diameter of short range connections. Each node is joined with every
  519. other node within this lattice distance.
  520. q : int
  521. The number of long-range connections for each node.
  522. r : float
  523. Exponent for decaying probability of connections. The probability of
  524. connecting to a node at lattice distance $d$ is $1/d^r$.
  525. dim : int
  526. Dimension of grid
  527. seed : integer, random_state, or None (default)
  528. Indicator of random number generation state.
  529. See :ref:`Randomness<randomness>`.
  530. References
  531. ----------
  532. .. [1] J. Kleinberg. The small-world phenomenon: An algorithmic
  533. perspective. Proc. 32nd ACM Symposium on Theory of Computing, 2000.
  534. """
  535. if p < 1:
  536. raise nx.NetworkXException("p must be >= 1")
  537. if q < 0:
  538. raise nx.NetworkXException("q must be >= 0")
  539. if r < 0:
  540. raise nx.NetworkXException("r must be >= 1")
  541. G = nx.DiGraph()
  542. nodes = list(product(range(n), repeat=dim))
  543. for p1 in nodes:
  544. probs = [0]
  545. for p2 in nodes:
  546. if p1 == p2:
  547. continue
  548. d = sum((abs(b - a) for a, b in zip(p1, p2)))
  549. if d <= p:
  550. G.add_edge(p1, p2)
  551. probs.append(d**-r)
  552. cdf = list(accumulate(probs))
  553. for _ in range(q):
  554. target = nodes[bisect_left(cdf, seed.uniform(0, cdf[-1]))]
  555. G.add_edge(p1, target)
  556. return G
  557. @py_random_state(7)
  558. def thresholded_random_geometric_graph(
  559. n, radius, theta, dim=2, pos=None, weight=None, p=2, seed=None
  560. ):
  561. r"""Returns a thresholded random geometric graph in the unit cube.
  562. The thresholded random geometric graph [1] model places `n` nodes
  563. uniformly at random in the unit cube of dimensions `dim`. Each node
  564. `u` is assigned a weight :math:`w_u`. Two nodes `u` and `v` are
  565. joined by an edge if they are within the maximum connection distance,
  566. `radius` computed by the `p`-Minkowski distance and the summation of
  567. weights :math:`w_u` + :math:`w_v` is greater than or equal
  568. to the threshold parameter `theta`.
  569. Edges within `radius` of each other are determined using a KDTree when
  570. SciPy is available. This reduces the time complexity from :math:`O(n^2)`
  571. to :math:`O(n)`.
  572. Parameters
  573. ----------
  574. n : int or iterable
  575. Number of nodes or iterable of nodes
  576. radius: float
  577. Distance threshold value
  578. theta: float
  579. Threshold value
  580. dim : int, optional
  581. Dimension of graph
  582. pos : dict, optional
  583. A dictionary keyed by node with node positions as values.
  584. weight : dict, optional
  585. Node weights as a dictionary of numbers keyed by node.
  586. p : float, optional (default 2)
  587. Which Minkowski distance metric to use. `p` has to meet the condition
  588. ``1 <= p <= infinity``.
  589. If this argument is not specified, the :math:`L^2` metric
  590. (the Euclidean distance metric), p = 2 is used.
  591. This should not be confused with the `p` of an Erdős-Rényi random
  592. graph, which represents probability.
  593. seed : integer, random_state, or None (default)
  594. Indicator of random number generation state.
  595. See :ref:`Randomness<randomness>`.
  596. Returns
  597. -------
  598. Graph
  599. A thresholded random geographic graph, undirected and without
  600. self-loops.
  601. Each node has a node attribute ``'pos'`` that stores the
  602. position of that node in Euclidean space as provided by the
  603. ``pos`` keyword argument or, if ``pos`` was not provided, as
  604. generated by this function. Similarly, each node has a nodethre
  605. attribute ``'weight'`` that stores the weight of that node as
  606. provided or as generated.
  607. Examples
  608. --------
  609. Default Graph:
  610. G = nx.thresholded_random_geometric_graph(50, 0.2, 0.1)
  611. Custom Graph:
  612. Create a thresholded random geometric graph on 50 uniformly distributed
  613. nodes where nodes are joined by an edge if their sum weights drawn from
  614. a exponential distribution with rate = 5 are >= theta = 0.1 and their
  615. Euclidean distance is at most 0.2.
  616. Notes
  617. -----
  618. This uses a *k*-d tree to build the graph.
  619. The `pos` keyword argument can be used to specify node positions so you
  620. can create an arbitrary distribution and domain for positions.
  621. For example, to use a 2D Gaussian distribution of node positions with mean
  622. (0, 0) and standard deviation 2
  623. If weights are not specified they are assigned to nodes by drawing randomly
  624. from the exponential distribution with rate parameter :math:`\lambda=1`.
  625. To specify weights from a different distribution, use the `weight` keyword
  626. argument::
  627. ::
  628. >>> import random
  629. >>> import math
  630. >>> n = 50
  631. >>> pos = {i: (random.gauss(0, 2), random.gauss(0, 2)) for i in range(n)}
  632. >>> w = {i: random.expovariate(5.0) for i in range(n)}
  633. >>> G = nx.thresholded_random_geometric_graph(n, 0.2, 0.1, 2, pos, w)
  634. References
  635. ----------
  636. .. [1] http://cole-maclean.github.io/blog/files/thesis.pdf
  637. """
  638. G = nx.empty_graph(n)
  639. G.name = f"thresholded_random_geometric_graph({n}, {radius}, {theta}, {dim})"
  640. # If no weights are provided, choose them from an exponential
  641. # distribution.
  642. if weight is None:
  643. weight = {v: seed.expovariate(1) for v in G}
  644. # If no positions are provided, choose uniformly random vectors in
  645. # Euclidean space of the specified dimension.
  646. if pos is None:
  647. pos = {v: [seed.random() for i in range(dim)] for v in G}
  648. # If no distance metric is provided, use Euclidean distance.
  649. nx.set_node_attributes(G, weight, "weight")
  650. nx.set_node_attributes(G, pos, "pos")
  651. edges = (
  652. (u, v)
  653. for u, v in _geometric_edges(G, radius, p)
  654. if weight[u] + weight[v] >= theta
  655. )
  656. G.add_edges_from(edges)
  657. return G