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